@herodevs/cli 2.0.0-beta.4 → 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.
- package/README.md +142 -108
- 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 +71 -0
- package/dist/commands/scan/eol.d.ts +12 -12
- package/dist/commands/scan/eol.js +163 -104
- package/dist/config/constants.d.ts +8 -3
- package/dist/config/constants.js +18 -3
- package/dist/hooks/finally.d.ts +3 -0
- package/dist/hooks/finally.js +14 -0
- package/dist/hooks/prerun.js +12 -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} +1 -1
- package/package.json +22 -15
- package/dist/api/client.d.ts +0 -12
- package/dist/api/client.js +0 -43
- package/dist/api/nes/nes.client.d.ts +0 -24
- package/dist/api/nes/nes.client.js +0 -127
- package/dist/api/queries/nes/sbom.d.ts +0 -3
- package/dist/api/queries/nes/sbom.js +0 -39
- 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 -58
- 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 -147
- package/dist/commands/report/purls.d.ts +0 -15
- package/dist/commands/report/purls.js +0 -85
- package/dist/commands/scan/sbom.d.ts +0 -21
- package/dist/commands/scan/sbom.js +0 -164
- 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 -12
- package/dist/service/eol/eol.svc.js +0 -25
- 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 -36
- package/dist/service/purls.svc.d.ts +0 -23
- package/dist/service/purls.svc.js +0 -99
- package/dist/ui/shared.ui.d.ts +0 -4
- package/dist/ui/shared.ui.js +0 -14
- /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
|
@@ -1,153 +1,212 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { batchSubmitPurls } from "../../api/nes/nes.client.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";
|
|
6
5
|
import { config, filenamePrefix } from "../../config/constants.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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
46
|
description: `Save the generated report as ${filenamePrefix}.report.json in the scanned directory`,
|
|
37
47
|
}),
|
|
48
|
+
saveSbom: Flags.boolean({
|
|
49
|
+
aliases: ['save-sbom'],
|
|
50
|
+
default: false,
|
|
51
|
+
description: `Save the generated SBOM as ${filenamePrefix}.sbom.json in the scanned directory`,
|
|
52
|
+
}),
|
|
38
53
|
};
|
|
39
54
|
async run() {
|
|
40
55
|
const { flags } = await this.parse(ScanEol);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}));
|
|
44
98
|
if (flags.save) {
|
|
45
|
-
|
|
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
|
+
}));
|
|
46
115
|
}
|
|
47
116
|
if (!this.jsonEnabled()) {
|
|
48
|
-
this.displayResults(
|
|
49
|
-
if (scan.scanId) {
|
|
50
|
-
this.printWebReportUrl(scan.scanId);
|
|
51
|
-
}
|
|
52
|
-
this.log('* Use --json to output the report payload');
|
|
53
|
-
this.log(`* Use --save to save the report to ${filenamePrefix}.report.json`);
|
|
54
|
-
this.log('* Use --help for more commands or options');
|
|
117
|
+
this.displayResults(scan);
|
|
55
118
|
}
|
|
56
|
-
return
|
|
119
|
+
return scan;
|
|
57
120
|
}
|
|
58
|
-
async
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
132
|
}
|
|
67
|
-
|
|
133
|
+
async scanSbom(sbom) {
|
|
134
|
+
const spinner = ora().start('Scanning for EOL packages');
|
|
68
135
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
136
|
+
const scan = await submitScan({ sbom: trimCdxBom(sbom) });
|
|
137
|
+
spinner.succeed('Scan completed');
|
|
138
|
+
return scan;
|
|
71
139
|
}
|
|
72
140
|
catch (error) {
|
|
73
|
-
|
|
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}`);
|
|
74
150
|
}
|
|
75
151
|
}
|
|
76
|
-
|
|
77
|
-
this.log(ux.colorize('bold', '-'.repeat(40)));
|
|
78
|
-
const id = scanId.split(SCAN_ID_KEY)[1];
|
|
79
|
-
const reportCardUrl = config.eolReportUrl;
|
|
80
|
-
const url = ux.colorize('blue', terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }));
|
|
81
|
-
this.log(`🌐 View your full EOL report at: ${url}\n`);
|
|
82
|
-
}
|
|
83
|
-
async scanSbom(sbom) {
|
|
84
|
-
let scan;
|
|
85
|
-
let purls;
|
|
152
|
+
saveReport(report, dir) {
|
|
86
153
|
try {
|
|
87
|
-
|
|
154
|
+
return saveReportToFile(dir, report);
|
|
88
155
|
}
|
|
89
156
|
catch (error) {
|
|
90
|
-
|
|
157
|
+
const errorMessage = getErrorMessage(error);
|
|
158
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
159
|
+
this.error(errorMessage);
|
|
91
160
|
}
|
|
161
|
+
}
|
|
162
|
+
saveSbom(dir, sbom) {
|
|
92
163
|
try {
|
|
93
|
-
|
|
164
|
+
return saveSbomToFile(dir, sbom);
|
|
94
165
|
}
|
|
95
166
|
catch (error) {
|
|
96
|
-
|
|
167
|
+
const errorMessage = getErrorMessage(error);
|
|
168
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
169
|
+
this.error(errorMessage);
|
|
97
170
|
}
|
|
98
|
-
return scan;
|
|
99
171
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2));
|
|
105
|
-
this.log(`Report saved to ${filenamePrefix}.report.json`);
|
|
172
|
+
displayResults(report) {
|
|
173
|
+
const lines = formatScanResults(report);
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
this.log(line);
|
|
106
176
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (error.code === 'EACCES') {
|
|
112
|
-
this.error(`Permission denied. Unable to save report to ${filenamePrefix}.report.json`);
|
|
113
|
-
}
|
|
114
|
-
else if (error.code === 'ENOSPC') {
|
|
115
|
-
this.error(`No space left on device. Unable to save report to ${filenamePrefix}.report.json`);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
177
|
+
if (report.id) {
|
|
178
|
+
const lines = formatWebReportUrl(report.id, config.eolReportUrl);
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
this.log(line);
|
|
119
181
|
}
|
|
120
182
|
}
|
|
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');
|
|
121
186
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
const errorMessage = getErrorMessage(error);
|
|
198
|
+
track('CLI Error Encountered', () => ({ error: errorMessage }));
|
|
199
|
+
this.error(`Failed to scan directory: ${errorMessage}`);
|
|
127
200
|
}
|
|
128
|
-
this.log(ux.colorize('bold', 'Scan results:'));
|
|
129
|
-
this.log(ux.colorize('bold', '-'.repeat(40)));
|
|
130
|
-
this.log(ux.colorize('bold', `${components.length.toLocaleString()} total packages scanned`));
|
|
131
|
-
this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} ${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`));
|
|
132
|
-
this.log(ux.colorize(STATUS_COLORS.EOL_UPCOMING, `${INDICATORS.EOL_UPCOMING}${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`));
|
|
133
|
-
this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} ${OK.toLocaleString().padEnd(5)} OK`));
|
|
134
|
-
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} ${UNKNOWN.toLocaleString().padEnd(5)} Unknown Status`));
|
|
135
|
-
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} ${NES_AVAILABLE.toLocaleString().padEnd(5)} HeroDevs NES Remediation${NES_AVAILABLE !== 1 ? 's' : ''} Available`));
|
|
136
201
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
for (const component of components) {
|
|
147
|
-
grouped[component.info.status]++;
|
|
148
|
-
if (component.info.nesAvailable) {
|
|
149
|
-
grouped.NES_AVAILABLE++;
|
|
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);
|
|
150
210
|
}
|
|
151
211
|
}
|
|
152
|
-
return grouped;
|
|
153
212
|
}
|
|
@@ -1,10 +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
|
};
|
|
10
15
|
export declare const filenamePrefix = "herodevs";
|
package/dist/config/constants.js
CHANGED
|
@@ -1,10 +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
|
};
|
|
10
25
|
export const filenamePrefix = 'herodevs';
|
|
@@ -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;
|
package/dist/hooks/prerun.js
CHANGED
|
@@ -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 {
|
|
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>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
-
import { debugLogger } from "
|
|
2
|
+
import { debugLogger } from "./log.svc.js";
|
|
3
|
+
const author = process.env.npm_package_author ?? 'HeroDevs, Inc.';
|
|
3
4
|
export const SBOM_DEFAULT__OPTIONS = {
|
|
4
5
|
$0: 'cdxgen',
|
|
5
6
|
_: [],
|
|
@@ -23,8 +24,8 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
23
24
|
includeFormulation: false,
|
|
24
25
|
'no-install-deps': true,
|
|
25
26
|
noInstallDeps: true,
|
|
26
|
-
'min-confidence':
|
|
27
|
-
minConfidence:
|
|
27
|
+
'min-confidence': 1,
|
|
28
|
+
minConfidence: 1,
|
|
28
29
|
multiProject: true,
|
|
29
30
|
'no-banner': false,
|
|
30
31
|
noBabel: false,
|
|
@@ -32,7 +33,7 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
32
33
|
o: 'bom.json',
|
|
33
34
|
output: 'bom.json',
|
|
34
35
|
outputFormat: 'json', // or "xml"
|
|
35
|
-
|
|
36
|
+
author: [author],
|
|
36
37
|
profile: 'generic',
|
|
37
38
|
project: undefined,
|
|
38
39
|
'project-version': '',
|
|
@@ -50,6 +51,13 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
50
51
|
skipDtTlsCheck: true,
|
|
51
52
|
'spec-version': 1.6,
|
|
52
53
|
specVersion: 1.6,
|
|
54
|
+
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: '@herodevs/cli',
|
|
57
|
+
publisher: author,
|
|
58
|
+
version: process.env.npm_package_version ?? 'unknown',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
53
61
|
'usages-slices-file': 'usages.slices.json',
|
|
54
62
|
usagesSlicesFile: 'usages.slices.json',
|
|
55
63
|
validate: true,
|
|
@@ -58,8 +66,10 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
58
66
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
59
67
|
* `directory`, and returns the `bomJson` property.
|
|
60
68
|
*/
|
|
61
|
-
export async function
|
|
62
|
-
const sbom = await createBom(directory,
|
|
69
|
+
export async function createSbom(directory) {
|
|
70
|
+
const sbom = await createBom(directory, SBOM_DEFAULT__OPTIONS);
|
|
71
|
+
if (!sbom)
|
|
72
|
+
throw new Error('SBOM not generated');
|
|
63
73
|
debugLogger('Successfully generated SBOM');
|
|
64
|
-
return sbom
|
|
74
|
+
return sbom.bomJson;
|
|
65
75
|
}
|