@herodevs/cli 2.0.0-beta.2 → 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
@@ -10,6 +10,14 @@ The HeroDevs CLI
10
10
  * [@herodevs/cli](#herodevscli)
11
11
  <!-- tocstop -->
12
12
 
13
+ ## Installation Instructions
14
+
15
+ 1. Install node v20 or higher: [Download Node](https://nodejs.org/en/download)
16
+ 1. Install the CLI using one of the following methods:
17
+ - Globally: Refer to the [Usage](#usage) instructions on installing the CLI globally
18
+ - Npx:`npx @herodevs/cli@beta <commands>`
19
+ 1. Refer to the [Commands](#commands) section for a list of commands
20
+
13
21
  ## TERMS
14
22
 
15
23
  Use of this CLI is governed by the [HeroDevs End of Life Dataset Terms of Service and Data Policy](https://docs.herodevs.com/legal/end-of-life-dataset-terms).
@@ -30,7 +38,7 @@ $ npm install -g @herodevs/cli
30
38
  $ hd COMMAND
31
39
  running command...
32
40
  $ hd (--version)
33
- @herodevs/cli/2.0.0-beta.2 linux-x64 node-v22.15.0
41
+ @herodevs/cli/2.0.0-beta.4 linux-x64 node-v22.16.0
34
42
  $ hd --help [COMMAND]
35
43
  USAGE
36
44
  $ hd COMMAND
@@ -64,7 +72,7 @@ DESCRIPTION
64
72
  Display help for hd.
65
73
  ```
66
74
 
67
- _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)_
68
76
 
69
77
  ## `hd report committers`
70
78
 
@@ -77,7 +85,7 @@ USAGE
77
85
  FLAGS
78
86
  -c, --csv Output in CSV format
79
87
  -m, --months=<value> [default: 12] The number of months of git history to review
80
- -s, --save Save the committers report as eol.committers.<output>
88
+ -s, --save Save the committers report as herodevs.committers.<output>
81
89
 
82
90
  GLOBAL FLAGS
83
91
  --json Format output as json.
@@ -95,7 +103,7 @@ EXAMPLES
95
103
  $ hd report committers --csv
96
104
  ```
97
105
 
98
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.2/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)_
99
107
 
100
108
  ## `hd report purls`
101
109
 
@@ -109,7 +117,7 @@ FLAGS
109
117
  -c, --csv Save output in CSV format (only applies when using --save)
110
118
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
111
119
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
112
- -s, --save Save the list of purls as eol.purls.<output>
120
+ -s, --save Save the list of purls as herodevs.purls.<output>
113
121
 
114
122
  GLOBAL FLAGS
115
123
  --json Format output as json.
@@ -129,7 +137,7 @@ EXAMPLES
129
137
  $ hd report purls --save --csv
130
138
  ```
131
139
 
132
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.2/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)_
133
141
 
134
142
  ## `hd scan eol`
135
143
 
@@ -137,15 +145,13 @@ Scan a given sbom for EOL data
137
145
 
138
146
  ```
139
147
  USAGE
140
- $ 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]
141
149
 
142
150
  FLAGS
143
- -a, --all Show all components (default is EOL and SUPPORTED only)
144
151
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
145
152
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
146
153
  -p, --purls=<value> The file path of a list of purls to scan for EOL
147
- -s, --save Save the generated report as eol.report.json in the scanned directory
148
- -t, --table Display the results in a table
154
+ -s, --save Save the generated report as herodevs.report.json in the scanned directory
149
155
 
150
156
  GLOBAL FLAGS
151
157
  --json Format output as json.
@@ -163,7 +169,7 @@ EXAMPLES
163
169
  $ hd scan eol -a --dir=./my-project
164
170
  ```
165
171
 
166
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.2/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)_
167
173
 
168
174
  ## `hd scan sbom`
169
175
 
@@ -177,7 +183,7 @@ FLAGS
177
183
  -b, --background Run the scan in the background
178
184
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
179
185
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
180
- -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
181
187
 
182
188
  GLOBAL FLAGS
183
189
  --json Format output as json.
@@ -191,7 +197,7 @@ EXAMPLES
191
197
  $ hd scan sbom --file=path/to/sbom.json
192
198
  ```
193
199
 
194
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.2/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)_
195
201
 
196
202
  ## `hd update [CHANNEL]`
197
203
 
@@ -229,5 +235,5 @@ EXAMPLES
229
235
  $ hd update --available
230
236
  ```
231
237
 
232
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.39/src/commands/update.ts)_
238
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.45/src/commands/update.ts)_
233
239
  <!-- commandsstop -->
package/bin/dev.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- process.env.GRAPHQL_HOST = 'https://api.dev.nes.herodevs.com';
4
- process.env.EOL_REPORT_URL = 'https://eol-report-card.stage.apps.herodevs.io/reports';
5
-
6
3
  import main from './main.js';
7
4
 
8
5
  try {
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('-')) {
@@ -16,6 +16,7 @@ export declare class NesApolloClient implements NesClient {
16
16
  query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
17
17
  }
18
18
  export declare const batchSubmitPurls: (purls: string[], options?: ScanInputOptions, batchSize?: number) => Promise<ScanResult>;
19
+ export declare const dedupeAndEncodePurls: (purls: string[]) => string[];
19
20
  export declare const createBatches: (items: string[], batchSize: number) => string[][];
20
21
  export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
21
22
  export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
@@ -1,3 +1,4 @@
1
+ import { PackageURL } from 'packageurl-js';
1
2
  import { ApolloClient } from "../../api/client.js";
2
3
  import { config } from "../../config/constants.js";
3
4
  import { debugLogger } from "../../service/log.svc.js";
@@ -25,12 +26,14 @@ function submitScan(purls, options) {
25
26
  const host = config.graphqlHost;
26
27
  const path = config.graphqlPath;
27
28
  const url = host + path;
29
+ debugLogger('Submitting scan to %s', url);
28
30
  const client = new NesApolloClient(url);
29
31
  return client.scan.purls(purls, options);
30
32
  }
31
33
  export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIONS, batchSize = DEFAULT_SCAN_BATCH_SIZE) => {
32
34
  try {
33
- const batches = createBatches(purls, batchSize);
35
+ const dedupedAndEncodedPurls = dedupeAndEncodePurls(purls);
36
+ const batches = createBatches(dedupedAndEncodedPurls, batchSize);
34
37
  debugLogger('Processing %d batches', batches.length);
35
38
  if (batches.length === 0) {
36
39
  return {
@@ -39,6 +42,7 @@ export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIO
39
42
  success: true,
40
43
  warnings: [],
41
44
  scanId: undefined,
45
+ createdOn: undefined,
42
46
  };
43
47
  }
44
48
  const results = await processBatches(batches, options);
@@ -49,6 +53,22 @@ export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIO
49
53
  throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`);
50
54
  }
51
55
  };
56
+ export const dedupeAndEncodePurls = (purls) => {
57
+ const dedupedAndEncodedPurls = new Set();
58
+ for (const purl of purls) {
59
+ try {
60
+ // The PackageURL.fromString method encodes each part of the purl
61
+ const encodedPurl = PackageURL.fromString(purl).toString();
62
+ if (!dedupedAndEncodedPurls.has(encodedPurl)) {
63
+ dedupedAndEncodedPurls.add(encodedPurl);
64
+ }
65
+ }
66
+ catch (error) {
67
+ debugLogger('Error encoding purl: %s', error);
68
+ }
69
+ }
70
+ return Array.from(dedupedAndEncodedPurls);
71
+ };
52
72
  export const createBatches = (items, batchSize) => {
53
73
  const numberOfBatches = Math.ceil(items.length / batchSize);
54
74
  return Array.from({ length: numberOfBatches }, (_, index) => {
@@ -15,10 +15,14 @@ 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
21
24
  scanId
25
+ createdOn
22
26
  success
23
27
  warnings {
24
28
  purl
@@ -8,13 +8,13 @@ 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>;
17
+ createdOn?: string;
18
18
  diagnostics?: Record<string, unknown>;
19
19
  message: string;
20
20
  success: boolean;
@@ -22,6 +22,7 @@ export interface ScanResponse {
22
22
  */
23
23
  export interface InsightsEolScanResult {
24
24
  scanId?: string;
25
+ createdOn: string;
25
26
  success: boolean;
26
27
  message: string;
27
28
  components: InsightsEolScanComponent[];
@@ -37,10 +38,14 @@ export interface InsightsEolScanComponentInfo {
37
38
  status: ComponentStatus;
38
39
  daysEol: number | null;
39
40
  vulnCount: number | null;
41
+ nesAvailable?: boolean;
40
42
  }
41
43
  export interface InsightsEolScanComponent {
42
44
  info: InsightsEolScanComponentInfo;
43
45
  purl: string;
46
+ remediation?: {
47
+ id: string;
48
+ } | null;
44
49
  }
45
50
  export interface ScanWarning {
46
51
  purl: string;
@@ -50,4 +55,4 @@ export interface ScanWarning {
50
55
  diagnostics?: Record<string, unknown>;
51
56
  }
52
57
  export type ComponentStatus = (typeof VALID_STATUSES)[number];
53
- 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,23 +9,16 @@ 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[];
15
+ createdOn: string;
17
16
  }>;
18
17
  private getScan;
19
18
  private getPurlsFromFile;
20
19
  private printWebReportUrl;
21
20
  private scanSbom;
22
- private getFilteredComponents;
23
21
  private saveReport;
24
22
  private displayResults;
25
- private displayResultsInTable;
26
- private displayTable;
27
- private displayNoComponentsMessage;
28
- private logLine;
29
- private displayStatusSection;
30
- private logLegend;
31
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,40 +33,27 @@ 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
- await this.saveReport(components);
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
- return { components };
56
+ return { components, createdOn: scan.createdOn ?? '' };
70
57
  }
71
58
  async getScan(flags, config) {
72
59
  if (flags.purls) {
@@ -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,92 +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
- async saveReport(components) {
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
- fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
124
- this.log('Report saved to eol.report.json');
104
+ fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2));
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
- 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)}`);
111
+ if (error.code === 'EACCES') {
112
+ this.error(`Permission denied. Unable to save report to ${filenamePrefix}.report.json`);
139
113
  }
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);
153
- }
154
- this.logLegend();
155
- }
156
- displayResultsInTable(scan, all) {
157
- const grouped = groupComponentsByStatus(scan.components);
158
- const statuses = ['SUPPORTED', 'EOL'];
159
- if (all) {
160
- statuses.unshift('UNKNOWN', 'OK');
161
- }
162
- for (const status of statuses) {
163
- const components = grouped[status];
164
- if (components.length > 0) {
165
- const table = createTableForStatus(grouped, status);
166
- this.displayTable(table, components.length, status);
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)}`);
167
119
  }
168
120
  }
169
- this.logLegend();
170
- }
171
- displayTable(table, count, status) {
172
- this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
173
- this.log(ux.colorize(STATUS_COLORS[status], table));
174
121
  }
175
- displayNoComponentsMessage(all) {
176
- if (!all) {
177
- this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
178
- this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
179
- }
180
- else {
122
+ displayResults(components) {
123
+ const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(components);
124
+ if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
181
125
  this.log(ux.colorize('yellow', 'No components found in scan.'));
126
+ return;
182
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`));
183
136
  }
184
- logLine() {
185
- this.log(ux.colorize('bold', '-'.repeat(50)));
186
- }
187
- displayStatusSection(components) {
188
- if (components.length > 0) {
189
- this.log(components.join('\n'));
190
- this.logLine();
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++;
191
150
  }
192
151
  }
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
- }
152
+ return grouped;
199
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,
@@ -11,6 +19,7 @@ export const buildScanResult = (scan) => {
11
19
  success: true,
12
20
  warnings: scan.warnings || [],
13
21
  scanId: scan.scanId,
22
+ createdOn: scan.createdOn,
14
23
  };
15
24
  };
16
25
  export const SbomScanner = (client) => async (purls, options) => {
@@ -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.2",
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.2.7",
41
- "@oclif/core": "^4",
42
- "@oclif/plugin-help": "^6",
43
- "@oclif/plugin-update": "^4",
44
- "@oclif/table": "^0.4.7",
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
- "@biomejs/biome": "^1.8.3",
51
- "@oclif/test": "^4",
50
+ "@biomejs/biome": "^1.9.4",
51
+ "@oclif/test": "^4.1.13",
52
52
  "@types/inquirer": "^9.0.8",
53
- "@types/node": "^22",
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",
57
+ "oclif": "^4.20.1",
58
58
  "shx": "^0.4.0",
59
59
  "sinon": "^20.0.0",
60
- "ts-node": "^10",
61
- "tsx": "^4.19.4",
60
+ "ts-node": "^10.9.2",
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
- }