@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 +20 -14
- package/bin/dev.js +0 -3
- package/bin/main.js +2 -2
- package/dist/api/nes/nes.client.d.ts +1 -0
- package/dist/api/nes/nes.client.js +21 -1
- package/dist/api/queries/nes/sbom.js +4 -0
- package/dist/api/types/hd-cli.types.d.ts +2 -2
- package/dist/api/types/nes.types.d.ts +6 -1
- package/dist/api/types/nes.types.js +1 -1
- package/dist/commands/report/committers.js +5 -4
- package/dist/commands/report/purls.js +3 -2
- package/dist/commands/scan/eol.d.ts +3 -10
- package/dist/commands/scan/eol.js +51 -97
- package/dist/commands/scan/sbom.js +12 -7
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.js +1 -0
- package/dist/service/eol/eol.svc.d.ts +0 -2
- package/dist/service/eol/eol.svc.js +0 -24
- package/dist/service/eol/sbom.worker.js +2 -1
- package/dist/service/nes/nes.svc.js +10 -1
- package/dist/service/purls.svc.d.ts +1 -1
- package/dist/service/purls.svc.js +1 -1
- package/dist/ui/shared.ui.d.ts +0 -2
- package/dist/ui/shared.ui.js +2 -4
- package/package.json +13 -13
- package/dist/ui/date.ui.d.ts +0 -1
- package/dist/ui/date.ui.js +0 -15
- package/dist/ui/eol.ui.d.ts +0 -15
- package/dist/ui/eol.ui.js +0 -134
package/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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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]
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
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
|
|
10
|
+
// If no arguments at all, default to scan:eol
|
|
11
11
|
if (positionals.length === 0) {
|
|
12
|
-
process.argv.splice(2, 0, 'scan:eol'
|
|
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
|
|
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) => {
|
|
@@ -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:
|
|
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", "
|
|
58
|
+
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "EOL_UPCOMING"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', '
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(),
|
|
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:
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(),
|
|
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(
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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}
|
|
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,
|
|
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}`);
|
package/dist/config/constants.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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) {
|
package/dist/ui/shared.ui.d.ts
CHANGED
|
@@ -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-";
|
package/dist/ui/shared.ui.js
CHANGED
|
@@ -3,14 +3,12 @@ export const STATUS_COLORS = {
|
|
|
3
3
|
EOL: 'red',
|
|
4
4
|
UNKNOWN: 'default',
|
|
5
5
|
OK: 'green',
|
|
6
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
|
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.
|
|
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.
|
|
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.
|
|
60
|
+
"ts-node": "^10.9.2",
|
|
61
|
+
"tsx": "^4.20.3",
|
|
62
62
|
"typescript": "^5.8.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
package/dist/ui/date.ui.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function parseMomentToSimpleDate(momentDate: string | Date | number | null): string;
|
package/dist/ui/date.ui.js
DELETED
|
@@ -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
|
-
}
|
package/dist/ui/eol.ui.d.ts
DELETED
|
@@ -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
|
-
}
|