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

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 CHANGED
@@ -38,7 +38,7 @@ $ npm install -g @herodevs/cli
38
38
  $ hd COMMAND
39
39
  running command...
40
40
  $ hd (--version)
41
- @herodevs/cli/2.0.0-beta.3 linux-x64 node-v22.16.0
41
+ @herodevs/cli/2.0.0-beta.4 linux-x64 node-v22.16.0
42
42
  $ hd --help [COMMAND]
43
43
  USAGE
44
44
  $ hd COMMAND
@@ -72,7 +72,7 @@ DESCRIPTION
72
72
  Display help for hd.
73
73
  ```
74
74
 
75
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.28/src/commands/help.ts)_
75
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.29/src/commands/help.ts)_
76
76
 
77
77
  ## `hd report committers`
78
78
 
@@ -85,7 +85,7 @@ USAGE
85
85
  FLAGS
86
86
  -c, --csv Output in CSV format
87
87
  -m, --months=<value> [default: 12] The number of months of git history to review
88
- -s, --save Save the committers report as eol.committers.<output>
88
+ -s, --save Save the committers report as herodevs.committers.<output>
89
89
 
90
90
  GLOBAL FLAGS
91
91
  --json Format output as json.
@@ -103,7 +103,7 @@ EXAMPLES
103
103
  $ hd report committers --csv
104
104
  ```
105
105
 
106
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.3/src/commands/report/committers.ts)_
106
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/report/committers.ts)_
107
107
 
108
108
  ## `hd report purls`
109
109
 
@@ -117,7 +117,7 @@ FLAGS
117
117
  -c, --csv Save output in CSV format (only applies when using --save)
118
118
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
119
119
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
120
- -s, --save Save the list of purls as eol.purls.<output>
120
+ -s, --save Save the list of purls as herodevs.purls.<output>
121
121
 
122
122
  GLOBAL FLAGS
123
123
  --json Format output as json.
@@ -137,7 +137,7 @@ EXAMPLES
137
137
  $ hd report purls --save --csv
138
138
  ```
139
139
 
140
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.3/src/commands/report/purls.ts)_
140
+ _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/report/purls.ts)_
141
141
 
142
142
  ## `hd scan eol`
143
143
 
@@ -145,15 +145,13 @@ Scan a given sbom for EOL data
145
145
 
146
146
  ```
147
147
  USAGE
148
- $ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-t]
148
+ $ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s]
149
149
 
150
150
  FLAGS
151
- -a, --all Show all components (default is EOL and SUPPORTED only)
152
151
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
153
152
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
154
153
  -p, --purls=<value> The file path of a list of purls to scan for EOL
155
- -s, --save Save the generated report as eol.report.json in the scanned directory
156
- -t, --table Display the results in a table
154
+ -s, --save Save the generated report as herodevs.report.json in the scanned directory
157
155
 
158
156
  GLOBAL FLAGS
159
157
  --json Format output as json.
@@ -171,7 +169,7 @@ EXAMPLES
171
169
  $ hd scan eol -a --dir=./my-project
172
170
  ```
173
171
 
174
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.3/src/commands/scan/eol.ts)_
172
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/scan/eol.ts)_
175
173
 
176
174
  ## `hd scan sbom`
177
175
 
@@ -185,7 +183,7 @@ FLAGS
185
183
  -b, --background Run the scan in the background
186
184
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
187
185
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
188
- -s, --save Save the generated SBOM as eol.sbom.json in the scanned directory
186
+ -s, --save Save the generated SBOM as herodevs.sbom.json in the scanned directory
189
187
 
190
188
  GLOBAL FLAGS
191
189
  --json Format output as json.
@@ -199,7 +197,7 @@ EXAMPLES
199
197
  $ hd scan sbom --file=path/to/sbom.json
200
198
  ```
201
199
 
202
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.3/src/commands/scan/sbom.ts)_
200
+ _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/scan/sbom.ts)_
203
201
 
204
202
  ## `hd update [CHANNEL]`
205
203
 
@@ -237,5 +235,5 @@ EXAMPLES
237
235
  $ hd update --available
238
236
  ```
239
237
 
240
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.42/src/commands/update.ts)_
238
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.45/src/commands/update.ts)_
241
239
  <!-- commandsstop -->
package/bin/main.js CHANGED
@@ -7,9 +7,9 @@ async function main(isProduction = false) {
7
7
  strict: false, // Don't validate flags
8
8
  });
9
9
 
10
- // If no arguments at all, default to scan:eol -t
10
+ // If no arguments at all, default to scan:eol
11
11
  if (positionals.length === 0) {
12
- process.argv.splice(2, 0, 'scan:eol', '-t');
12
+ process.argv.splice(2, 0, 'scan:eol');
13
13
  }
14
14
  // If only flags are provided, set scan:eol as the command for those flags
15
15
  else if (positionals.length === 1 && positionals[0].startsWith('-')) {
@@ -15,6 +15,9 @@ export const M_SCAN = {
15
15
  status
16
16
  vulnCount
17
17
  }
18
+ remediation {
19
+ id
20
+ }
18
21
  }
19
22
  diagnostics
20
23
  message
@@ -8,13 +8,12 @@ export interface ScanInputOptions {
8
8
  }
9
9
  export declare const DEFAULT_SCAN_BATCH_SIZE = 1000;
10
10
  export declare const DEFAULT_SCAN_INPUT_OPTIONS: ScanInputOptions;
11
- export type ScanResultComponentsMap = Map<string, InsightsEolScanComponent>;
12
11
  export type ScanInput = {
13
12
  components: string[];
14
13
  options: ScanInputOptions;
15
14
  };
16
15
  export interface ScanResult {
17
- components: ScanResultComponentsMap;
16
+ components: Map<string, InsightsEolScanComponent>;
18
17
  createdOn?: string;
19
18
  diagnostics?: Record<string, unknown>;
20
19
  message: string;
@@ -38,10 +38,14 @@ export interface InsightsEolScanComponentInfo {
38
38
  status: ComponentStatus;
39
39
  daysEol: number | null;
40
40
  vulnCount: number | null;
41
+ nesAvailable?: boolean;
41
42
  }
42
43
  export interface InsightsEolScanComponent {
43
44
  info: InsightsEolScanComponentInfo;
44
45
  purl: string;
46
+ remediation?: {
47
+ id: string;
48
+ } | null;
45
49
  }
46
50
  export interface ScanWarning {
47
51
  purl: string;
@@ -51,4 +55,4 @@ export interface ScanWarning {
51
55
  diagnostics?: Record<string, unknown>;
52
56
  }
53
57
  export type ComponentStatus = (typeof VALID_STATUSES)[number];
54
- export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "SUPPORTED"];
58
+ export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "EOL_UPCOMING"];
@@ -1 +1 @@
1
- export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'SUPPORTED'];
1
+ export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'EOL_UPCOMING'];
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { Command, Flags } from '@oclif/core';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { filenamePrefix } from "../../config/constants.js";
5
6
  import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
6
7
  import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
7
8
  export default class Committers extends Command {
@@ -26,7 +27,7 @@ export default class Committers extends Command {
26
27
  }),
27
28
  save: Flags.boolean({
28
29
  char: 's',
29
- description: 'Save the committers report as eol.committers.<output>',
30
+ description: `Save the committers report as ${filenamePrefix}.committers.<output>`,
30
31
  default: false,
31
32
  }),
32
33
  };
@@ -46,7 +47,7 @@ export default class Committers extends Command {
46
47
  // JSON mode
47
48
  if (save) {
48
49
  try {
49
- fs.writeFileSync(path.resolve('eol.committers.json'), JSON.stringify(reportData, null, 2));
50
+ fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.json`), JSON.stringify(reportData, null, 2));
50
51
  this.log('Report written to json');
51
52
  }
52
53
  catch (error) {
@@ -61,7 +62,7 @@ export default class Committers extends Command {
61
62
  const csvOutput = formatAsCsv(reportData);
62
63
  if (save) {
63
64
  try {
64
- fs.writeFileSync(path.resolve('eol.committers.csv'), csvOutput);
65
+ fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.csv`), csvOutput);
65
66
  this.log('Report written to csv');
66
67
  }
67
68
  catch (error) {
@@ -75,7 +76,7 @@ export default class Committers extends Command {
75
76
  }
76
77
  if (save) {
77
78
  try {
78
- fs.writeFileSync(path.resolve('eol.committers.txt'), textOutput);
79
+ fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.txt`), textOutput);
79
80
  this.log('Report written to txt');
80
81
  }
81
82
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command, Flags, ux } from '@oclif/core';
4
+ import { filenamePrefix } from "../../config/constants.js";
4
5
  import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
5
6
  import { extractPurls, getPurlOutput } from "../../service/purls.svc.js";
6
7
  import ScanSbom from "../scan/sbom.js";
@@ -26,7 +27,7 @@ export default class ReportPurls extends Command {
26
27
  save: Flags.boolean({
27
28
  char: 's',
28
29
  default: false,
29
- description: 'Save the list of purls as eol.purls.<output>',
30
+ description: `Save the list of purls as ${filenamePrefix}.purls.<output>`,
30
31
  }),
31
32
  csv: Flags.boolean({
32
33
  char: 'c',
@@ -50,7 +51,7 @@ export default class ReportPurls extends Command {
50
51
  if (save) {
51
52
  try {
52
53
  const outputFile = csv && !this.jsonEnabled() ? 'csv' : 'json';
53
- const outputPath = path.join(_dirFlag || process.cwd(), `eol.purls.${outputFile}`);
54
+ const outputPath = path.join(_dirFlag || process.cwd(), `${filenamePrefix}.purls.${outputFile}`);
54
55
  const purlOutput = getPurlOutput(purls, outputFile);
55
56
  fs.writeFileSync(outputPath, purlOutput);
56
57
  this.log('Purls saved to %s', outputPath);
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import type { InsightsEolScanComponent } from '../../api/types/nes.types.ts';
2
+ import type { ComponentStatus, InsightsEolScanComponent } from '../../api/types/nes.types.ts';
3
3
  export default class ScanEol extends Command {
4
4
  static description: string;
5
5
  static enableJsonFlag: boolean;
@@ -9,8 +9,6 @@ export default class ScanEol extends Command {
9
9
  purls: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
- all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
12
  };
15
13
  run(): Promise<{
16
14
  components: InsightsEolScanComponent[];
@@ -20,13 +18,7 @@ export default class ScanEol extends Command {
20
18
  private getPurlsFromFile;
21
19
  private printWebReportUrl;
22
20
  private scanSbom;
23
- private getFilteredComponents;
24
21
  private saveReport;
25
22
  private displayResults;
26
- private displayResultsInTable;
27
- private displayTable;
28
- private displayNoComponentsMessage;
29
- private logLine;
30
- private displayStatusSection;
31
- private logLegend;
32
23
  }
24
+ export declare function countComponentsByStatus(components: InsightsEolScanComponent[]): Record<ComponentStatus | 'NES_AVAILABLE', number>;
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command, Flags, ux } from '@oclif/core';
4
+ import terminalLink from 'terminal-link';
4
5
  import { batchSubmitPurls } from "../../api/nes/nes.client.js";
5
- import { config } from "../../config/constants.js";
6
+ import { config, filenamePrefix } from "../../config/constants.js";
6
7
  import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
7
8
  import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
8
- import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
9
9
  import { INDICATORS, SCAN_ID_KEY, STATUS_COLORS } from "../../ui/shared.ui.js";
10
10
  import ScanSbom from "./sbom.js";
11
11
  export default class ScanEol extends Command {
@@ -33,38 +33,25 @@ export default class ScanEol extends Command {
33
33
  save: Flags.boolean({
34
34
  char: 's',
35
35
  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,
42
- }),
43
- table: Flags.boolean({
44
- char: 't',
45
- description: 'Display the results in a table',
46
- default: false,
36
+ description: `Save the generated report as ${filenamePrefix}.report.json in the scanned directory`,
47
37
  }),
48
38
  };
49
39
  async run() {
50
40
  const { flags } = await this.parse(ScanEol);
51
41
  const scan = await this.getScan(flags, this.config);
52
- ux.action.stop('\nScan completed');
53
- const components = this.getFilteredComponents(scan, flags.all);
42
+ const components = Array.from(scan.components.values());
43
+ ux.action.stop();
54
44
  if (flags.save) {
55
45
  await this.saveReport(components, scan.createdOn);
56
46
  }
57
47
  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
- }
48
+ this.displayResults(components);
65
49
  if (scan.scanId) {
66
50
  this.printWebReportUrl(scan.scanId);
67
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');
68
55
  }
69
56
  return { components, createdOn: scan.createdOn ?? '' };
70
57
  }
@@ -87,11 +74,11 @@ export default class ScanEol extends Command {
87
74
  }
88
75
  }
89
76
  printWebReportUrl(scanId) {
90
- this.logLine();
77
+ this.log(ux.colorize('bold', '-'.repeat(40)));
91
78
  const id = scanId.split(SCAN_ID_KEY)[1];
92
79
  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)}`);
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`);
95
82
  }
96
83
  async scanSbom(sbom) {
97
84
  let scan;
@@ -108,91 +95,59 @@ export default class ScanEol extends Command {
108
95
  catch (error) {
109
96
  this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
110
97
  }
111
- if (scan.components.size === 0) {
112
- this.warn('No components found in scan');
113
- }
114
98
  return scan;
115
99
  }
116
- getFilteredComponents(scan, all) {
117
- return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
118
- }
119
100
  async saveReport(components, createdOn) {
120
101
  const { flags } = await this.parse(ScanEol);
121
- const reportPath = path.join(flags.dir || process.cwd(), 'eol.report.json');
102
+ const reportPath = path.join(flags.dir || process.cwd(), `${filenamePrefix}.report.json`);
122
103
  try {
123
104
  fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2));
124
- this.log('Report saved to eol.report.json');
105
+ this.log(`Report saved to ${filenamePrefix}.report.json`);
125
106
  }
126
107
  catch (error) {
127
108
  if (!isErrnoException(error)) {
128
109
  this.error(`Failed to save report: ${getErrorMessage(error)}`);
129
110
  }
130
111
  if (error.code === 'EACCES') {
131
- this.error('Permission denied. Unable to save report to eol.report.json');
112
+ this.error(`Permission denied. Unable to save report to ${filenamePrefix}.report.json`);
132
113
  }
133
114
  else if (error.code === 'ENOSPC') {
134
- this.error('No space left on device. Unable to save report to eol.report.json');
115
+ this.error(`No space left on device. Unable to save report to ${filenamePrefix}.report.json`);
135
116
  }
136
117
  else {
137
118
  this.error(`Failed to save report: ${getErrorMessage(error)}`);
138
119
  }
139
120
  }
140
121
  }
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);
152
- }
153
- this.logLegend();
154
- }
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);
166
- }
167
- }
168
- this.logLegend();
169
- }
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.'));
178
- }
179
- else {
122
+ displayResults(components) {
123
+ const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(components);
124
+ if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
180
125
  this.log(ux.colorize('yellow', 'No components found in scan.'));
126
+ return;
181
127
  }
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`));
182
136
  }
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();
137
+ }
138
+ export function countComponentsByStatus(components) {
139
+ const grouped = {
140
+ UNKNOWN: 0,
141
+ OK: 0,
142
+ EOL_UPCOMING: 0,
143
+ EOL: 0,
144
+ NES_AVAILABLE: 0,
145
+ };
146
+ for (const component of components) {
147
+ grouped[component.info.status]++;
148
+ if (component.info.nesAvailable) {
149
+ grouped.NES_AVAILABLE++;
190
150
  }
191
151
  }
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
- }
152
+ return grouped;
198
153
  }
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { Command, Flags, ux } from '@oclif/core';
5
+ import { filenamePrefix } from "../../config/constants.js";
5
6
  import { createSbom, validateIsCycloneDxSbom } from "../../service/eol/eol.svc.js";
6
7
  import { getErrorMessage } from "../../service/error.svc.js";
7
8
  export default class ScanSbom extends Command {
@@ -23,7 +24,7 @@ export default class ScanSbom extends Command {
23
24
  save: Flags.boolean({
24
25
  char: 's',
25
26
  default: false,
26
- description: 'Save the generated SBOM as eol.sbom.json in the scanned directory',
27
+ description: `Save the generated SBOM as ${filenamePrefix}.sbom.json in the scanned directory`,
27
28
  }),
28
29
  background: Flags.boolean({
29
30
  char: 'b',
@@ -41,15 +42,13 @@ export default class ScanSbom extends Command {
41
42
  return sbom;
42
43
  }
43
44
  static getSbomArgs(flags) {
44
- const { dir, file, save, json, background } = flags ?? {};
45
- const sbomArgs = [];
45
+ const { dir, file, background } = flags ?? {};
46
+ const sbomArgs = ['--json'];
46
47
  if (file)
47
48
  sbomArgs.push('--file', file);
48
49
  if (dir)
49
50
  sbomArgs.push('--dir', dir);
50
51
  // if (save) sbomArgs.push('--save'); // only save if sbom command is used directly with -s flag
51
- if (json)
52
- sbomArgs.push('--json');
53
52
  if (background)
54
53
  sbomArgs.push('--background');
55
54
  return sbomArgs;
@@ -69,18 +68,24 @@ export default class ScanSbom extends Command {
69
68
  const path = dir || process.cwd();
70
69
  if (file) {
71
70
  sbom = this._getSbomFromFile(file);
71
+ ux.action.stop();
72
72
  }
73
73
  else if (background) {
74
74
  this._getSbomInBackground(path);
75
- this.log(`The scan is running in the background. The file will be saved at ${path}/eol.sbom.json`);
75
+ this.log(`The scan is running in the background. The file will be saved at ${path}/${filenamePrefix}.sbom.json`);
76
+ ux.action.stop();
76
77
  return;
77
78
  }
78
79
  else {
79
80
  sbom = await this._getSbomFromScan(path);
81
+ ux.action.stop();
80
82
  if (save) {
81
83
  this._saveSbom(path, sbom);
82
84
  }
83
85
  }
86
+ if (!save) {
87
+ this.log(JSON.stringify(sbom, null, 2));
88
+ }
84
89
  return sbom;
85
90
  }
86
91
  async _getSbomFromScan(_dirFlag) {
@@ -146,7 +151,7 @@ export default class ScanSbom extends Command {
146
151
  }
147
152
  _saveSbom(dir, sbom) {
148
153
  try {
149
- const outputPath = join(dir, 'eol.sbom.json');
154
+ const outputPath = join(dir, `${filenamePrefix}.sbom.json`);
150
155
  fs.writeFileSync(outputPath, JSON.stringify(sbom, null, 2));
151
156
  if (!this.jsonEnabled()) {
152
157
  this.log(`SBOM saved to ${outputPath}`);
@@ -7,3 +7,4 @@ export declare const config: {
7
7
  graphqlPath: string;
8
8
  showVulnCount: boolean;
9
9
  };
10
+ export declare const filenamePrefix = "herodevs";
@@ -7,3 +7,4 @@ export const config = {
7
7
  graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH,
8
8
  showVulnCount: true,
9
9
  };
10
+ export const filenamePrefix = 'herodevs';
@@ -1,4 +1,3 @@
1
- import type { PackageURL } from 'packageurl-js';
2
1
  import { type Sbom } from './cdx.svc.ts';
3
2
  export interface CdxGenOptions {
4
3
  projectType?: string[];
@@ -11,4 +10,3 @@ export type CdxCreator = (dir: string, opts: CdxGenOptions) => Promise<{
11
10
  }>;
12
11
  export declare function createSbom(directory: string, opts?: ScanOptions): Promise<any>;
13
12
  export declare function validateIsCycloneDxSbom(sbom: unknown): asserts sbom is Sbom;
14
- export declare function resolvePurlPackageName(purl: PackageURL): string;
@@ -23,27 +23,3 @@ export function validateIsCycloneDxSbom(sbom) {
23
23
  throw new Error('Invalid SBOM: missing or invalid components array');
24
24
  }
25
25
  }
26
- const purlPackageNameRules = {
27
- npm: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
28
- maven: (p) => (p.namespace ? `${p.namespace}:${p.name}` : p.name),
29
- pypi: (p) => p.name.toLowerCase(),
30
- nuget: (p) => p.name,
31
- gem: (p) => p.name,
32
- composer: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
33
- golang: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
34
- cargo: (p) => p.name,
35
- conan: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
36
- github: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
37
- bitbucket: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
38
- docker: (p) => (p.namespace ? `${p.namespace}/${p.name}` : p.name),
39
- };
40
- function isKnownEcosystemType(type) {
41
- return type in purlPackageNameRules;
42
- }
43
- export function resolvePurlPackageName(purl) {
44
- if (!isKnownEcosystemType(purl.type)) {
45
- debugLogger(`Unsupported package type: ${purl.type}, falling back to name only`);
46
- return purl.name;
47
- }
48
- return purlPackageNameRules[purl.type](purl);
49
- }
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { createBom } from '@cyclonedx/cdxgen';
4
+ import { filenamePrefix } from "../../config/constants.js";
4
5
  import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
5
6
  process.on('uncaughtException', (err) => {
6
7
  console.error('Uncaught exception:', err.message);
@@ -15,7 +16,7 @@ try {
15
16
  const options = JSON.parse(process.argv[2]);
16
17
  const { path, opts } = options;
17
18
  const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
18
- const outputPath = join(path, 'eol.sbom.json');
19
+ const outputPath = join(path, `${filenamePrefix}.sbom.json`);
19
20
  writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
20
21
  process.exit(0);
21
22
  }
@@ -3,7 +3,15 @@ import { debugLogger } from "../log.svc.js";
3
3
  export const buildScanResult = (scan) => {
4
4
  const components = new Map();
5
5
  for (const c of scan.components) {
6
- components.set(c.purl, c);
6
+ const status = c.info.status;
7
+ components.set(c.purl, {
8
+ info: {
9
+ ...c.info,
10
+ nesAvailable: c.remediation !== null,
11
+ status: status === 'SUPPORTED' ? 'EOL_UPCOMING' : status,
12
+ },
13
+ purl: c.purl,
14
+ });
7
15
  }
8
16
  return {
9
17
  components,
@@ -17,7 +17,7 @@ export declare function getPurlOutput(purls: string[], output: string): string;
17
17
  export declare function extractPurls(sbom: Sbom): string[];
18
18
  /**
19
19
  * Parse a purls file in either JSON or text format, including the format of
20
- * eol.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
20
+ * herodevs.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
21
21
  * or a text file with one purl per line.
22
22
  */
23
23
  export declare function parsePurlsFile(purlsFileString: string): string[];
@@ -62,7 +62,7 @@ export function extractPurls(sbom) {
62
62
  }
63
63
  /**
64
64
  * Parse a purls file in either JSON or text format, including the format of
65
- * eol.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
65
+ * herodevs.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
66
66
  * or a text file with one purl per line.
67
67
  */
68
68
  export function parsePurlsFile(purlsFileString) {
@@ -1,6 +1,4 @@
1
1
  import type { ComponentStatus } from '../api/types/nes.types.ts';
2
2
  export declare const STATUS_COLORS: Record<ComponentStatus, string>;
3
3
  export declare const INDICATORS: Record<ComponentStatus, string>;
4
- export declare const MAX_PURL_LENGTH = 60;
5
- export declare const MAX_TABLE_COLUMN_WIDTH = 30;
6
4
  export declare const SCAN_ID_KEY = "eol-scan-v1-";
@@ -3,14 +3,12 @@ export const STATUS_COLORS = {
3
3
  EOL: 'red',
4
4
  UNKNOWN: 'default',
5
5
  OK: 'green',
6
- SUPPORTED: 'yellow',
6
+ EOL_UPCOMING: 'yellow',
7
7
  };
8
8
  export const INDICATORS = {
9
9
  EOL: ux.colorize(STATUS_COLORS.EOL, '✗'),
10
10
  UNKNOWN: ux.colorize(STATUS_COLORS.UNKNOWN, '•'),
11
11
  OK: ux.colorize(STATUS_COLORS.OK, '✔'),
12
- SUPPORTED: ux.colorize(STATUS_COLORS.SUPPORTED, '⚡'),
12
+ EOL_UPCOMING: ux.colorize(STATUS_COLORS.EOL_UPCOMING, '⚡'),
13
13
  };
14
- export const MAX_PURL_LENGTH = 60;
15
- export const MAX_TABLE_COLUMN_WIDTH = 30;
16
14
  export const SCAN_ID_KEY = 'eol-scan-v1-';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herodevs/cli",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "author": "HeroDevs, Inc",
5
5
  "bin": {
6
6
  "hd": "./bin/run.js"
@@ -17,7 +17,7 @@
17
17
  "ci": "biome ci",
18
18
  "ci:fix": "biome check --write",
19
19
  "clean": "shx rm -rf dist && npm run clean:files && shx rm -rf node_modules",
20
- "clean:files": "shx rm -f eol.**.csv eol.**.json eol.**.text",
20
+ "clean:files": "shx rm -f herodevs.**.csv herodevs.**.json herodevs.**.txt",
21
21
  "dev": "npm run build && ./bin/dev.js",
22
22
  "dev:debug": "npm run build && DEBUG=oclif:* ./bin/dev.js",
23
23
  "format": "biome format --write",
@@ -37,28 +37,28 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@apollo/client": "^3.13.8",
40
- "@cyclonedx/cdxgen": "^11.3.2",
41
- "@oclif/core": "^4.3.1",
42
- "@oclif/plugin-help": "^6.2.28",
43
- "@oclif/plugin-update": "^4.6.42",
44
- "@oclif/table": "^0.4.8",
40
+ "@cyclonedx/cdxgen": "^11.4.1",
41
+ "@oclif/core": "^4.4.0",
42
+ "@oclif/plugin-help": "^6.2.29",
43
+ "@oclif/plugin-update": "^4.6.45",
45
44
  "graphql": "^16.11.0",
46
45
  "packageurl-js": "^2.0.1",
46
+ "terminal-link": "^4.0.0",
47
47
  "update-notifier": "^7.3.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@biomejs/biome": "^1.9.4",
51
51
  "@oclif/test": "^4.1.13",
52
52
  "@types/inquirer": "^9.0.8",
53
- "@types/node": "^22.15.30",
53
+ "@types/node": "^22.15.32",
54
54
  "@types/sinon": "^17.0.4",
55
55
  "@types/update-notifier": "^6.0.8",
56
56
  "globstar": "^1.0.0",
57
- "oclif": "^4.18.0",
57
+ "oclif": "^4.20.1",
58
58
  "shx": "^0.4.0",
59
59
  "sinon": "^20.0.0",
60
60
  "ts-node": "^10.9.2",
61
- "tsx": "^4.19.4",
61
+ "tsx": "^4.20.3",
62
62
  "typescript": "^5.8.3"
63
63
  },
64
64
  "engines": {
@@ -1 +0,0 @@
1
- export declare function parseMomentToSimpleDate(momentDate: string | Date | number | null): string;
@@ -1,15 +0,0 @@
1
- export function parseMomentToSimpleDate(momentDate) {
2
- // Only return empty string for null
3
- if (momentDate === null)
4
- return '';
5
- try {
6
- const dateObj = new Date(momentDate);
7
- if (Number.isNaN(dateObj.getTime())) {
8
- throw new Error('Invalid date');
9
- }
10
- return dateObj.toISOString().split('T')[0];
11
- }
12
- catch {
13
- throw new Error('Invalid date');
14
- }
15
- }
@@ -1,15 +0,0 @@
1
- import type { ScanResultComponentsMap } from '../api/types/hd-cli.types.ts';
2
- import type { ComponentStatus, InsightsEolScanComponent } from '../api/types/nes.types.ts';
3
- export declare function truncateString(purl: string, maxLength: number): string;
4
- export declare function colorizeStatus(status: ComponentStatus): string;
5
- export declare function createStatusDisplay(components: ScanResultComponentsMap, all: boolean): Record<ComponentStatus, string[]>;
6
- export declare function createTableForStatus(grouped: Record<ComponentStatus, InsightsEolScanComponent[]>, status: ComponentStatus): string;
7
- export declare function convertComponentToTableRow(component: InsightsEolScanComponent): {
8
- name: string;
9
- version: string;
10
- eol: string;
11
- daysEol: number | null;
12
- type: string;
13
- vulnCount: number | null;
14
- };
15
- export declare function groupComponentsByStatus(components: ScanResultComponentsMap): Record<ComponentStatus, InsightsEolScanComponent[]>;
package/dist/ui/eol.ui.js DELETED
@@ -1,134 +0,0 @@
1
- import { ux } from '@oclif/core';
2
- import { makeTable } from '@oclif/table';
3
- import { PackageURL } from 'packageurl-js';
4
- import { config } from "../config/constants.js";
5
- import { resolvePurlPackageName } from "../service/eol/eol.svc.js";
6
- import { parseMomentToSimpleDate } from "./date.ui.js";
7
- import { INDICATORS, MAX_PURL_LENGTH, MAX_TABLE_COLUMN_WIDTH, STATUS_COLORS } from "./shared.ui.js";
8
- export function truncateString(purl, maxLength) {
9
- const ellipses = '...';
10
- return purl.length > maxLength ? `${purl.slice(0, maxLength - ellipses.length)}${ellipses}` : purl;
11
- }
12
- export function colorizeStatus(status) {
13
- return ux.colorize(STATUS_COLORS[status], status);
14
- }
15
- function formatSimpleComponent(purl, status) {
16
- const color = STATUS_COLORS[status];
17
- return ` ${INDICATORS[status]} ${ux.colorize(color, truncateString(purl, MAX_PURL_LENGTH))}`;
18
- }
19
- function getDaysEolString(daysEol) {
20
- if (daysEol === null) {
21
- return '';
22
- }
23
- if (daysEol <= 0) {
24
- return `${Math.abs(daysEol) + 1} days from now`;
25
- }
26
- if (daysEol > 0) {
27
- return 'today';
28
- }
29
- return `${daysEol} days ago`;
30
- }
31
- function formatDetailedComponent(purl, info) {
32
- const { status, eolAt, daysEol, vulnCount } = info;
33
- const simpleComponent = formatSimpleComponent(purl, status);
34
- const eolAtString = parseMomentToSimpleDate(eolAt);
35
- const daysEolString = getDaysEolString(daysEol);
36
- const eolString = [`${simpleComponent}`, ` ⮑ EOL Date: ${eolAtString} (${daysEolString})`];
37
- if (config.showVulnCount) {
38
- eolString.push(` ⮑ # of Vulns: ${vulnCount ?? ''}`);
39
- }
40
- const output = eolString.filter(Boolean).join('\n');
41
- return output;
42
- }
43
- export function createStatusDisplay(components, all) {
44
- const statusOutput = {
45
- UNKNOWN: [],
46
- OK: [],
47
- SUPPORTED: [],
48
- EOL: [],
49
- };
50
- // Single loop to separate and format components
51
- for (const [purl, component] of components.entries()) {
52
- const { status } = component.info;
53
- if (all) {
54
- if (status === 'UNKNOWN' || status === 'OK') {
55
- statusOutput[status].push(formatSimpleComponent(purl, status));
56
- }
57
- }
58
- if (status === 'SUPPORTED' || status === 'EOL') {
59
- statusOutput[status].push(formatDetailedComponent(purl, component.info));
60
- }
61
- }
62
- return statusOutput;
63
- }
64
- export function createTableForStatus(grouped, status) {
65
- const data = grouped[status].map((component) => convertComponentToTableRow(component));
66
- if (status === 'EOL' || status === 'SUPPORTED') {
67
- if (config.showVulnCount) {
68
- return makeTable({
69
- data,
70
- columns: [
71
- { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
72
- { key: 'version', name: 'VERSION', width: 10 },
73
- { key: 'eol', name: 'EOL', width: 12 },
74
- { key: 'daysEol', name: 'DAYS EOL', width: 10 },
75
- { key: 'type', name: 'TYPE', width: 12 },
76
- { key: 'vulnCount', name: '# OF VULNS', width: 12 },
77
- ],
78
- });
79
- }
80
- return makeTable({
81
- data,
82
- columns: [
83
- { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
84
- { key: 'version', name: 'VERSION', width: 10 },
85
- { key: 'eol', name: 'EOL', width: 12 },
86
- { key: 'daysEol', name: 'DAYS EOL', width: 10 },
87
- { key: 'type', name: 'TYPE', width: 12 },
88
- ],
89
- });
90
- }
91
- if (config.showVulnCount) {
92
- return makeTable({
93
- data,
94
- columns: [
95
- { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
96
- { key: 'version', name: 'VERSION', width: 10 },
97
- { key: 'type', name: 'TYPE', width: 12 },
98
- { key: 'vulnCount', name: '# OF VULNS', width: 12 },
99
- ],
100
- });
101
- }
102
- return makeTable({
103
- data,
104
- columns: [
105
- { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
106
- { key: 'version', name: 'VERSION', width: 10 },
107
- { key: 'type', name: 'TYPE', width: 12 },
108
- ],
109
- });
110
- }
111
- export function convertComponentToTableRow(component) {
112
- const purlParts = PackageURL.fromString(component.purl);
113
- const { eolAt, daysEol, vulnCount } = component.info;
114
- return {
115
- name: resolvePurlPackageName(purlParts),
116
- version: purlParts.version ?? '',
117
- eol: parseMomentToSimpleDate(eolAt),
118
- daysEol: daysEol,
119
- type: purlParts.type,
120
- vulnCount: vulnCount,
121
- };
122
- }
123
- export function groupComponentsByStatus(components) {
124
- const grouped = {
125
- UNKNOWN: [],
126
- OK: [],
127
- SUPPORTED: [],
128
- EOL: [],
129
- };
130
- for (const component of components.values()) {
131
- grouped[component.info.status].push(component);
132
- }
133
- return grouped;
134
- }