@herodevs/cli 1.4.0-beta.1 → 1.5.0-beta.1
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 +13 -13
- package/dist/api/nes/nes.client.d.ts +2 -2
- package/dist/api/nes/nes.client.js +3 -1
- package/dist/api/queries/nes/sbom.js +6 -4
- package/dist/api/types/nes.types.d.ts +2 -1
- package/dist/api/types/nes.types.js +1 -1
- package/dist/commands/scan/eol.d.ts +3 -1
- package/dist/commands/scan/eol.js +64 -53
- package/dist/hooks/npm-update-notifier.d.ts +8 -0
- package/dist/hooks/npm-update-notifier.js +39 -0
- package/dist/service/nes/nes.svc.js +6 -0
- package/dist/ui/eol.ui.d.ts +12 -2
- package/dist/ui/eol.ui.js +56 -13
- package/dist/ui/{shared.us.d.ts → shared.ui.d.ts} +2 -0
- package/dist/ui/{shared.us.js → shared.ui.js} +4 -2
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ $ npm install -g @herodevs/cli
|
|
|
16
16
|
$ hd COMMAND
|
|
17
17
|
running command...
|
|
18
18
|
$ hd (--version)
|
|
19
|
-
@herodevs/cli/1.
|
|
19
|
+
@herodevs/cli/1.5.0-beta.1 linux-x64 node-v22.14.0
|
|
20
20
|
$ hd --help [COMMAND]
|
|
21
21
|
USAGE
|
|
22
22
|
$ hd COMMAND
|
|
@@ -81,7 +81,7 @@ EXAMPLES
|
|
|
81
81
|
$ hd report committers --csv
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.
|
|
84
|
+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/report/committers.ts)_
|
|
85
85
|
|
|
86
86
|
## `hd report purls`
|
|
87
87
|
|
|
@@ -115,7 +115,7 @@ EXAMPLES
|
|
|
115
115
|
$ hd report purls --save --csv
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.
|
|
118
|
+
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/report/purls.ts)_
|
|
119
119
|
|
|
120
120
|
## `hd scan eol`
|
|
121
121
|
|
|
@@ -123,15 +123,15 @@ Scan a given sbom for EOL data
|
|
|
123
123
|
|
|
124
124
|
```
|
|
125
125
|
USAGE
|
|
126
|
-
$ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-
|
|
126
|
+
$ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-t]
|
|
127
127
|
|
|
128
128
|
FLAGS
|
|
129
|
-
-a, --all
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
129
|
+
-a, --all Show all components (default is EOL and SUPPORTED only)
|
|
130
|
+
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
131
|
+
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
132
|
+
-p, --purls=<value> The file path of a list of purls to scan for EOL
|
|
133
|
+
-s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
|
|
134
|
+
-t, --table Display the results in a table
|
|
135
135
|
|
|
136
136
|
GLOBAL FLAGS
|
|
137
137
|
--json Format output as json.
|
|
@@ -149,7 +149,7 @@ EXAMPLES
|
|
|
149
149
|
$ hd scan eol -a --dir=./my-project
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.
|
|
152
|
+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/scan/eol.ts)_
|
|
153
153
|
|
|
154
154
|
## `hd scan sbom`
|
|
155
155
|
|
|
@@ -177,7 +177,7 @@ EXAMPLES
|
|
|
177
177
|
$ hd scan sbom --file=path/to/sbom.json
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.
|
|
180
|
+
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.5.0-beta.1/src/commands/scan/sbom.ts)_
|
|
181
181
|
|
|
182
182
|
## `hd update [CHANNEL]`
|
|
183
183
|
|
|
@@ -215,5 +215,5 @@ EXAMPLES
|
|
|
215
215
|
$ hd update --available
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.
|
|
218
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.37/src/commands/update.ts)_
|
|
219
219
|
<!-- commandsstop -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as apollo from '@apollo/client/core/index.js';
|
|
2
2
|
import type { InsightsEolScanInput, InsightsEolScanResult } from '../../api/types/nes.types.ts';
|
|
3
|
-
import type
|
|
3
|
+
import { type ProcessBatchOptions, type ScanInputOptions, type ScanResult } from '../types/hd-cli.types.ts';
|
|
4
4
|
export interface NesClient {
|
|
5
5
|
scan: {
|
|
6
6
|
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
@@ -15,7 +15,7 @@ export declare class NesApolloClient implements NesClient {
|
|
|
15
15
|
mutate<T, V extends Record<string, unknown>>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
16
16
|
query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
17
17
|
}
|
|
18
|
-
export declare const batchSubmitPurls: (purls: string[], options
|
|
18
|
+
export declare const batchSubmitPurls: (purls: string[], options?: ScanInputOptions, batchSize?: number) => Promise<ScanResult>;
|
|
19
19
|
export declare const createBatches: (items: string[], batchSize: number) => string[][];
|
|
20
20
|
export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
|
|
21
21
|
export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ApolloClient } from "../../api/client.js";
|
|
2
2
|
import { debugLogger } from "../../service/log.svc.js";
|
|
3
3
|
import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
|
|
4
|
+
import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS, } from "../types/hd-cli.types.js";
|
|
4
5
|
export class NesApolloClient {
|
|
5
6
|
scan = {
|
|
6
7
|
purls: SbomScanner(this),
|
|
@@ -27,7 +28,7 @@ function submitScan(purls, options) {
|
|
|
27
28
|
const client = new NesApolloClient(url);
|
|
28
29
|
return client.scan.purls(purls, options);
|
|
29
30
|
}
|
|
30
|
-
export const batchSubmitPurls = async (purls, options, batchSize) => {
|
|
31
|
+
export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIONS, batchSize = DEFAULT_SCAN_BATCH_SIZE) => {
|
|
31
32
|
try {
|
|
32
33
|
const batches = createBatches(purls, batchSize);
|
|
33
34
|
debugLogger('Processing %d batches', batches.length);
|
|
@@ -61,6 +62,7 @@ export const processBatch = async ({ batch, index, totalPages, scanOptions, prev
|
|
|
61
62
|
throw new Error('Total pages exceeded');
|
|
62
63
|
}
|
|
63
64
|
debugLogger('Processing batch %d of %d', page, totalPages);
|
|
65
|
+
debugLogger('ScanID: %s', previousScanId);
|
|
64
66
|
const result = await submitScan(batch, {
|
|
65
67
|
...scanOptions,
|
|
66
68
|
page,
|
|
@@ -2,9 +2,9 @@ import { gql } from '@apollo/client/core/core.cjs';
|
|
|
2
2
|
export const M_SCAN = {
|
|
3
3
|
gql: gql `
|
|
4
4
|
mutation EolScan($input: InsightsEolScanInput!) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
insights {
|
|
6
|
+
scan {
|
|
7
|
+
eol(input: $input) {
|
|
8
8
|
components {
|
|
9
9
|
purl
|
|
10
10
|
info {
|
|
@@ -13,8 +13,10 @@ export const M_SCAN = {
|
|
|
13
13
|
eolAt
|
|
14
14
|
daysEol
|
|
15
15
|
status
|
|
16
|
+
# TODO: uncomment vulnCount once backend changes are deployed
|
|
17
|
+
# vulnCount
|
|
16
18
|
}
|
|
17
|
-
}
|
|
19
|
+
}
|
|
18
20
|
diagnostics
|
|
19
21
|
message
|
|
20
22
|
scanId
|
|
@@ -36,6 +36,7 @@ export interface InsightsEolScanComponentInfo {
|
|
|
36
36
|
eolAt: Date | null;
|
|
37
37
|
status: ComponentStatus;
|
|
38
38
|
daysEol: number | null;
|
|
39
|
+
vulnCount: number | null;
|
|
39
40
|
}
|
|
40
41
|
export interface InsightsEolScanComponent {
|
|
41
42
|
info: InsightsEolScanComponentInfo;
|
|
@@ -49,4 +50,4 @@ export interface ScanWarning {
|
|
|
49
50
|
diagnostics?: Record<string, unknown>;
|
|
50
51
|
}
|
|
51
52
|
export type ComponentStatus = (typeof VALID_STATUSES)[number];
|
|
52
|
-
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "
|
|
53
|
+
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "SUPPORTED"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', '
|
|
1
|
+
export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'SUPPORTED'];
|
|
@@ -10,7 +10,7 @@ export default class ScanEol extends Command {
|
|
|
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
12
|
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
-
|
|
13
|
+
table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<{
|
|
16
16
|
components: InsightsEolScanComponent[];
|
|
@@ -21,6 +21,8 @@ export default class ScanEol extends Command {
|
|
|
21
21
|
private getFilteredComponents;
|
|
22
22
|
private saveReport;
|
|
23
23
|
private displayResults;
|
|
24
|
+
private displayResultsInTable;
|
|
25
|
+
private displayTable;
|
|
24
26
|
private displayNoComponentsMessage;
|
|
25
27
|
private logLine;
|
|
26
28
|
private displayStatusSection;
|
|
@@ -2,12 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command, Flags, ux } from '@oclif/core';
|
|
4
4
|
import { batchSubmitPurls } from "../../api/nes/nes.client.js";
|
|
5
|
-
import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS } from '../../api/types/hd-cli.types.js';
|
|
6
5
|
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
7
|
-
import { extractPurls } from "../../service/purls.svc.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { INDICATORS, STATUS_COLORS } from "../../ui/shared.us.js";
|
|
6
|
+
import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
|
|
7
|
+
import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
|
|
8
|
+
import { INDICATORS, STATUS_COLORS } from "../../ui/shared.ui.js";
|
|
11
9
|
import ScanSbom from "./sbom.js";
|
|
12
10
|
export default class ScanEol extends Command {
|
|
13
11
|
static description = 'Scan a given sbom for EOL data';
|
|
@@ -38,46 +36,44 @@ export default class ScanEol extends Command {
|
|
|
38
36
|
}),
|
|
39
37
|
all: Flags.boolean({
|
|
40
38
|
char: 'a',
|
|
41
|
-
description: 'Show all components (default is EOL and
|
|
39
|
+
description: 'Show all components (default is EOL and SUPPORTED only)',
|
|
42
40
|
default: false,
|
|
43
41
|
}),
|
|
44
|
-
|
|
45
|
-
char: '
|
|
46
|
-
description: '
|
|
42
|
+
table: Flags.boolean({
|
|
43
|
+
char: 't',
|
|
44
|
+
description: 'Display the results in a table',
|
|
47
45
|
default: false,
|
|
48
46
|
}),
|
|
49
47
|
};
|
|
50
48
|
async run() {
|
|
51
49
|
const { flags } = await this.parse(ScanEol);
|
|
52
|
-
if (flags.getCustomerSupport) {
|
|
53
|
-
this.log(ux.colorize('yellow', 'Never-Ending Support is on the way. Please stay tuned for this feature.'));
|
|
54
|
-
}
|
|
55
50
|
const scan = await this.getScan(flags, this.config);
|
|
56
51
|
ux.action.stop('\nScan completed');
|
|
57
|
-
const
|
|
52
|
+
const components = this.getFilteredComponents(scan, flags.all);
|
|
58
53
|
if (flags.save) {
|
|
59
|
-
await this.saveReport(
|
|
54
|
+
await this.saveReport(components);
|
|
60
55
|
}
|
|
61
|
-
if (this.jsonEnabled()) {
|
|
62
|
-
|
|
56
|
+
if (!this.jsonEnabled()) {
|
|
57
|
+
if (flags.table) {
|
|
58
|
+
this.log(`${scan.components.size} components scanned`);
|
|
59
|
+
this.displayResultsInTable(scan, flags.all);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.displayResults(scan, flags.all);
|
|
63
|
+
}
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
return { components: filteredComponents };
|
|
65
|
+
return { components };
|
|
66
66
|
}
|
|
67
67
|
async getScan(flags, config) {
|
|
68
68
|
if (flags.purls) {
|
|
69
69
|
ux.action.start(`Scanning purls from ${flags.purls}`);
|
|
70
70
|
const purls = this.getPurlsFromFile(flags.purls);
|
|
71
|
-
return batchSubmitPurls(purls
|
|
71
|
+
return batchSubmitPurls(purls);
|
|
72
72
|
}
|
|
73
73
|
const sbom = await ScanSbom.loadSbom(flags, config);
|
|
74
|
-
|
|
75
|
-
return scan;
|
|
74
|
+
return this.scanSbom(sbom);
|
|
76
75
|
}
|
|
77
76
|
getPurlsFromFile(filePath) {
|
|
78
|
-
if (typeof filePath !== 'string') {
|
|
79
|
-
this.error(`Failed to parse file path: ${filePath}`);
|
|
80
|
-
}
|
|
81
77
|
try {
|
|
82
78
|
const purlsFileString = fs.readFileSync(filePath, 'utf8');
|
|
83
79
|
return parsePurlsFile(purlsFileString);
|
|
@@ -86,7 +82,7 @@ export default class ScanEol extends Command {
|
|
|
86
82
|
this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
|
|
87
83
|
}
|
|
88
84
|
}
|
|
89
|
-
async scanSbom(sbom
|
|
85
|
+
async scanSbom(sbom) {
|
|
90
86
|
let scan;
|
|
91
87
|
let purls;
|
|
92
88
|
try {
|
|
@@ -96,7 +92,7 @@ export default class ScanEol extends Command {
|
|
|
96
92
|
this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
|
|
97
93
|
}
|
|
98
94
|
try {
|
|
99
|
-
scan = await batchSubmitPurls(purls
|
|
95
|
+
scan = await batchSubmitPurls(purls);
|
|
100
96
|
}
|
|
101
97
|
catch (error) {
|
|
102
98
|
this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
|
|
@@ -107,52 +103,67 @@ export default class ScanEol extends Command {
|
|
|
107
103
|
return scan;
|
|
108
104
|
}
|
|
109
105
|
getFilteredComponents(scan, all) {
|
|
110
|
-
return Array.from(scan.components.
|
|
111
|
-
.filter(([_, component]) => all || ['EOL', 'LTS'].includes(component.info.status))
|
|
112
|
-
.map(([_, component]) => component);
|
|
106
|
+
return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
|
|
113
107
|
}
|
|
114
108
|
async saveReport(components) {
|
|
109
|
+
const { flags } = await this.parse(ScanEol);
|
|
110
|
+
const reportPath = path.join(flags.dir || process.cwd(), 'nes.eol.json');
|
|
115
111
|
try {
|
|
116
|
-
const { flags } = await this.parse(ScanEol);
|
|
117
|
-
const reportPath = path.join(flags.dir || process.cwd(), 'nes.eol.json');
|
|
118
112
|
fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
|
|
119
113
|
this.log('Report saved to nes.eol.json');
|
|
120
114
|
}
|
|
121
115
|
catch (error) {
|
|
122
|
-
if (isErrnoException(error)) {
|
|
123
|
-
switch (error.code) {
|
|
124
|
-
case 'EACCES':
|
|
125
|
-
this.error('Permission denied. Unable to save report to nes.eol.json');
|
|
126
|
-
break;
|
|
127
|
-
case 'ENOSPC':
|
|
128
|
-
this.error('No space left on device. Unable to save report to nes.eol.json');
|
|
129
|
-
break;
|
|
130
|
-
default:
|
|
131
|
-
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
116
|
+
if (!isErrnoException(error)) {
|
|
135
117
|
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
136
118
|
}
|
|
119
|
+
switch (error.code) {
|
|
120
|
+
case 'EACCES':
|
|
121
|
+
this.error('Permission denied. Unable to save report to nes.eol.json');
|
|
122
|
+
break;
|
|
123
|
+
case 'ENOSPC':
|
|
124
|
+
this.error('No space left on device. Unable to save report to nes.eol.json');
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
128
|
+
}
|
|
137
129
|
}
|
|
138
130
|
}
|
|
139
|
-
|
|
140
|
-
const { UNKNOWN, OK,
|
|
141
|
-
if (!UNKNOWN.length && !OK.length && !
|
|
131
|
+
displayResults(scan, all) {
|
|
132
|
+
const { UNKNOWN, OK, SUPPORTED, EOL } = createStatusDisplay(scan.components, all);
|
|
133
|
+
if (!UNKNOWN.length && !OK.length && !SUPPORTED.length && !EOL.length) {
|
|
142
134
|
this.displayNoComponentsMessage(all);
|
|
143
135
|
return;
|
|
144
136
|
}
|
|
145
137
|
this.log(ux.colorize('bold', 'Here are the results of the scan:'));
|
|
146
138
|
this.logLine();
|
|
147
139
|
// Display sections in order of increasing severity
|
|
148
|
-
for (const components of [UNKNOWN, OK,
|
|
140
|
+
for (const components of [UNKNOWN, OK, SUPPORTED, EOL]) {
|
|
149
141
|
this.displayStatusSection(components);
|
|
150
142
|
}
|
|
151
143
|
this.logLegend();
|
|
152
144
|
}
|
|
145
|
+
displayResultsInTable(scan, all) {
|
|
146
|
+
const grouped = groupComponentsByStatus(scan.components);
|
|
147
|
+
const statuses = ['SUPPORTED', 'EOL'];
|
|
148
|
+
if (all) {
|
|
149
|
+
statuses.unshift('UNKNOWN', 'OK');
|
|
150
|
+
}
|
|
151
|
+
for (const status of statuses) {
|
|
152
|
+
const components = grouped[status];
|
|
153
|
+
if (components.length > 0) {
|
|
154
|
+
const table = createTableForStatus(grouped, status);
|
|
155
|
+
this.displayTable(table, components.length, status);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this.logLegend();
|
|
159
|
+
}
|
|
160
|
+
displayTable(table, count, status) {
|
|
161
|
+
this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
|
|
162
|
+
this.log(ux.colorize(STATUS_COLORS[status], table));
|
|
163
|
+
}
|
|
153
164
|
displayNoComponentsMessage(all) {
|
|
154
165
|
if (!all) {
|
|
155
|
-
this.log(ux.colorize('yellow', 'No End-of-Life or
|
|
166
|
+
this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
|
|
156
167
|
this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
|
|
157
168
|
}
|
|
158
169
|
else {
|
|
@@ -169,9 +180,9 @@ export default class ScanEol extends Command {
|
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
logLegend() {
|
|
172
|
-
this.log(ux.colorize(
|
|
173
|
-
this.log(ux.colorize(
|
|
174
|
-
this.log(ux.colorize(
|
|
175
|
-
this.log(ux.colorize(
|
|
183
|
+
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} = No Known Issues`));
|
|
184
|
+
this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} = OK`));
|
|
185
|
+
this.log(ux.colorize(STATUS_COLORS.SUPPORTED, `${INDICATORS.SUPPORTED}= Supported: End-of-Life (EOL) is scheduled`));
|
|
186
|
+
this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} = End of Life (EOL)`));
|
|
176
187
|
}
|
|
177
188
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Hook } from '@oclif/core';
|
|
2
|
+
import { type UpdateInfo } from 'update-notifier';
|
|
3
|
+
declare const updateNotifierHook: Hook.Init;
|
|
4
|
+
export default updateNotifierHook;
|
|
5
|
+
export declare function handleUpdate(update: UpdateInfo, currentVersion: string): {
|
|
6
|
+
message: string;
|
|
7
|
+
defer: boolean;
|
|
8
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import updateNotifier, {} from 'update-notifier';
|
|
2
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
3
|
+
const updateNotifierHook = async (options) => {
|
|
4
|
+
const notifier = updateNotifier({
|
|
5
|
+
pkg,
|
|
6
|
+
updateCheckInterval: 1000 * 60 * 60 * 24, // Check once per day
|
|
7
|
+
});
|
|
8
|
+
if (notifier.update) {
|
|
9
|
+
const notification = handleUpdate(notifier.update, pkg.version);
|
|
10
|
+
if (notification) {
|
|
11
|
+
notifier.notify(notification);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export default updateNotifierHook;
|
|
16
|
+
export function handleUpdate(update, currentVersion) {
|
|
17
|
+
const isPreV1 = currentVersion.startsWith('0.');
|
|
18
|
+
const isBeta = currentVersion.includes('-beta') || update.latest.includes('-beta');
|
|
19
|
+
const isAlpha = currentVersion.includes('-alpha') || update.latest.includes('-alpha');
|
|
20
|
+
const isNext = currentVersion.includes('-next') || update.latest.includes('-next');
|
|
21
|
+
let message = `Update available! v${currentVersion} → v${update.latest}`;
|
|
22
|
+
/**
|
|
23
|
+
* Show breaking changes warning for:
|
|
24
|
+
* - v0.x.x versions (all updates can contain breaking changes per SemVer spec[1][2])
|
|
25
|
+
* - Prerelease versions (beta/alpha/next)
|
|
26
|
+
* - Major version updates
|
|
27
|
+
*
|
|
28
|
+
* [1]https://semver.org/#spec-item-4
|
|
29
|
+
* [2]https://antfu.me/posts/epoch-semver#leading-zero-major-versioning
|
|
30
|
+
*/
|
|
31
|
+
if (isPreV1 || isBeta || isAlpha || isNext || update.type === 'major') {
|
|
32
|
+
message += '\nThis update may contain breaking changes.';
|
|
33
|
+
}
|
|
34
|
+
// For all other updates (minor, patch), they should be non-breaking
|
|
35
|
+
return {
|
|
36
|
+
message,
|
|
37
|
+
defer: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -3,6 +3,12 @@ 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
|
+
const { status } = c.info;
|
|
7
|
+
// TODO: remove this once backend changes are deployed
|
|
8
|
+
// @ts-expect-error
|
|
9
|
+
if (status === 'LTS') {
|
|
10
|
+
c.info.status = 'SUPPORTED';
|
|
11
|
+
}
|
|
6
12
|
components.set(c.purl, c);
|
|
7
13
|
}
|
|
8
14
|
return {
|
package/dist/ui/eol.ui.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { ScanResultComponentsMap } from '../api/types/hd-cli.types.ts';
|
|
2
|
-
import type { ComponentStatus } from '../api/types/nes.types.ts';
|
|
3
|
-
export declare function
|
|
2
|
+
import type { ComponentStatus, InsightsEolScanComponent } from '../api/types/nes.types.ts';
|
|
3
|
+
export declare function truncateString(purl: string, maxLength: number): string;
|
|
4
4
|
export declare function colorizeStatus(status: ComponentStatus): string;
|
|
5
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
CHANGED
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
|
+
import { makeTable } from '@oclif/table';
|
|
3
|
+
import { PackageURL } from 'packageurl-js';
|
|
2
4
|
import { parseMomentToSimpleDate } from "./date.ui.js";
|
|
3
|
-
import { INDICATORS, STATUS_COLORS } from "./shared.
|
|
4
|
-
export function
|
|
5
|
-
|
|
5
|
+
import { INDICATORS, MAX_PURL_LENGTH, MAX_TABLE_COLUMN_WIDTH, STATUS_COLORS } from "./shared.ui.js";
|
|
6
|
+
export function truncateString(purl, maxLength) {
|
|
7
|
+
const ellipses = '...';
|
|
8
|
+
return purl.length > maxLength ? `${purl.slice(0, maxLength - ellipses.length)}${ellipses}` : purl;
|
|
6
9
|
}
|
|
7
10
|
export function colorizeStatus(status) {
|
|
8
11
|
return ux.colorize(STATUS_COLORS[status], status);
|
|
9
12
|
}
|
|
10
13
|
function formatSimpleComponent(purl, status) {
|
|
11
14
|
const color = STATUS_COLORS[status];
|
|
12
|
-
return ` ${INDICATORS[status]} ${ux.colorize(color,
|
|
15
|
+
return ` ${INDICATORS[status]} ${ux.colorize(color, truncateString(purl, MAX_PURL_LENGTH))}`;
|
|
13
16
|
}
|
|
14
17
|
function getDaysEolString(daysEol) {
|
|
15
|
-
// UNKNOWN || OK
|
|
16
18
|
if (daysEol === null) {
|
|
17
19
|
return '';
|
|
18
20
|
}
|
|
19
|
-
// LTS
|
|
20
21
|
if (daysEol < 0) {
|
|
21
22
|
return `${Math.abs(daysEol)} days from now`;
|
|
22
23
|
}
|
|
23
|
-
// EOL
|
|
24
24
|
if (daysEol === 0) {
|
|
25
25
|
return 'today';
|
|
26
26
|
}
|
|
27
27
|
return `${daysEol} days ago`;
|
|
28
28
|
}
|
|
29
|
-
function formatDetailedComponent(purl,
|
|
29
|
+
function formatDetailedComponent(purl, info) {
|
|
30
|
+
const { status, eolAt, daysEol, vulnCount } = info;
|
|
30
31
|
const simpleComponent = formatSimpleComponent(purl, status);
|
|
31
32
|
const eolAtString = parseMomentToSimpleDate(eolAt);
|
|
32
33
|
const daysEolString = getDaysEolString(daysEol);
|
|
33
|
-
const output = [
|
|
34
|
+
const output = [
|
|
35
|
+
`${simpleComponent}`,
|
|
36
|
+
` ⮑ EOL Date: ${eolAtString} (${daysEolString})`,
|
|
37
|
+
` ⮑ # of Vulns: ${vulnCount ?? ''}`,
|
|
38
|
+
]
|
|
34
39
|
.filter(Boolean)
|
|
35
40
|
.join('\n');
|
|
36
41
|
return output;
|
|
@@ -39,20 +44,58 @@ export function createStatusDisplay(components, all) {
|
|
|
39
44
|
const statusOutput = {
|
|
40
45
|
UNKNOWN: [],
|
|
41
46
|
OK: [],
|
|
42
|
-
|
|
47
|
+
SUPPORTED: [],
|
|
43
48
|
EOL: [],
|
|
44
49
|
};
|
|
45
50
|
// Single loop to separate and format components
|
|
46
51
|
for (const [purl, component] of components.entries()) {
|
|
47
|
-
const { status
|
|
52
|
+
const { status } = component.info;
|
|
48
53
|
if (all) {
|
|
49
54
|
if (status === 'UNKNOWN' || status === 'OK') {
|
|
50
55
|
statusOutput[status].push(formatSimpleComponent(purl, status));
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
|
-
if (status === '
|
|
54
|
-
statusOutput[status].push(formatDetailedComponent(purl,
|
|
58
|
+
if (status === 'SUPPORTED' || status === 'EOL') {
|
|
59
|
+
statusOutput[status].push(formatDetailedComponent(purl, component.info));
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
return statusOutput;
|
|
58
63
|
}
|
|
64
|
+
export function createTableForStatus(grouped, status) {
|
|
65
|
+
const data = grouped[status].map((component) => convertComponentToTableRow(component));
|
|
66
|
+
return makeTable({
|
|
67
|
+
data,
|
|
68
|
+
columns: [
|
|
69
|
+
{ key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH },
|
|
70
|
+
{ key: 'version', name: 'VERSION', width: 10 },
|
|
71
|
+
{ key: 'eol', name: 'EOL', width: 12 },
|
|
72
|
+
{ key: 'daysEol', name: 'DAYS EOL', width: 10 },
|
|
73
|
+
{ key: 'type', name: 'TYPE', width: 12 },
|
|
74
|
+
{ key: 'vulnCount', name: '# OF VULNS', width: 12 },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function convertComponentToTableRow(component) {
|
|
79
|
+
const purlParts = PackageURL.fromString(component.purl);
|
|
80
|
+
const { eolAt, daysEol, vulnCount } = component.info;
|
|
81
|
+
return {
|
|
82
|
+
name: purlParts.name,
|
|
83
|
+
version: purlParts.version ?? '',
|
|
84
|
+
eol: parseMomentToSimpleDate(eolAt),
|
|
85
|
+
daysEol: daysEol,
|
|
86
|
+
type: purlParts.type,
|
|
87
|
+
vulnCount: vulnCount,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function groupComponentsByStatus(components) {
|
|
91
|
+
const grouped = {
|
|
92
|
+
UNKNOWN: [],
|
|
93
|
+
OK: [],
|
|
94
|
+
SUPPORTED: [],
|
|
95
|
+
EOL: [],
|
|
96
|
+
};
|
|
97
|
+
for (const component of components.values()) {
|
|
98
|
+
grouped[component.info.status].push(component);
|
|
99
|
+
}
|
|
100
|
+
return grouped;
|
|
101
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
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;
|
|
@@ -3,11 +3,13 @@ export const STATUS_COLORS = {
|
|
|
3
3
|
EOL: 'red',
|
|
4
4
|
UNKNOWN: 'default',
|
|
5
5
|
OK: 'green',
|
|
6
|
-
|
|
6
|
+
SUPPORTED: '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
|
-
|
|
12
|
+
SUPPORTED: ux.colorize(STATUS_COLORS.SUPPORTED, '⚡'),
|
|
13
13
|
};
|
|
14
|
+
export const MAX_PURL_LENGTH = 60;
|
|
15
|
+
export const MAX_TABLE_COLUMN_WIDTH = 30;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-beta.1",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"herodevs cli"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@apollo/client": "^3.13.
|
|
41
|
-
"@cyclonedx/cdxgen": "^11.2.
|
|
40
|
+
"@apollo/client": "^3.13.7",
|
|
41
|
+
"@cyclonedx/cdxgen": "^11.2.4",
|
|
42
42
|
"@oclif/core": "^4",
|
|
43
43
|
"@oclif/plugin-help": "^6",
|
|
44
44
|
"@oclif/plugin-update": "^4",
|
|
45
|
-
"
|
|
45
|
+
"@oclif/table": "^0.4.7",
|
|
46
|
+
"graphql": "^16.8.1",
|
|
47
|
+
"packageurl-js": "^2.0.1",
|
|
48
|
+
"update-notifier": "^7.3.1"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@biomejs/biome": "^1.8.3",
|
|
@@ -50,13 +53,14 @@
|
|
|
50
53
|
"@types/inquirer": "^9.0.7",
|
|
51
54
|
"@types/node": "^22",
|
|
52
55
|
"@types/sinon": "^17.0.4",
|
|
56
|
+
"@types/update-notifier": "^6.0.8",
|
|
53
57
|
"globstar": "^1.0.0",
|
|
54
58
|
"oclif": "^4",
|
|
55
|
-
"shx": "^0.
|
|
56
|
-
"sinon": "^
|
|
59
|
+
"shx": "^0.4.0",
|
|
60
|
+
"sinon": "^20.0.0",
|
|
57
61
|
"ts-node": "^10",
|
|
58
62
|
"tsx": "^4.19.3",
|
|
59
|
-
"typescript": "^5.8.
|
|
63
|
+
"typescript": "^5.8.3"
|
|
60
64
|
},
|
|
61
65
|
"engines": {
|
|
62
66
|
"node": ">=20.0.0"
|
|
@@ -78,6 +82,7 @@
|
|
|
78
82
|
"@oclif/plugin-update"
|
|
79
83
|
],
|
|
80
84
|
"hooks": {
|
|
85
|
+
"init": "./dist/hooks/npm-update-notifier",
|
|
81
86
|
"prerun": "./dist/hooks/prerun.js"
|
|
82
87
|
},
|
|
83
88
|
"topicSeparator": " ",
|