@herodevs/cli 2.0.0-beta.3 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/bin/main.js +2 -2
- package/dist/api/queries/nes/sbom.js +3 -0
- package/dist/api/types/hd-cli.types.d.ts +1 -2
- package/dist/api/types/nes.types.d.ts +5 -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 +2 -10
- package/dist/commands/scan/eol.js +42 -87
- 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 +9 -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 +10 -10
- 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
|
@@ -38,7 +38,7 @@ $ npm install -g @herodevs/cli
|
|
|
38
38
|
$ hd COMMAND
|
|
39
39
|
running command...
|
|
40
40
|
$ hd (--version)
|
|
41
|
-
@herodevs/cli/2.0.0-beta.
|
|
41
|
+
@herodevs/cli/2.0.0-beta.4 linux-x64 node-v22.16.0
|
|
42
42
|
$ hd --help [COMMAND]
|
|
43
43
|
USAGE
|
|
44
44
|
$ hd COMMAND
|
|
@@ -72,7 +72,7 @@ DESCRIPTION
|
|
|
72
72
|
Display help for hd.
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.
|
|
75
|
+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.29/src/commands/help.ts)_
|
|
76
76
|
|
|
77
77
|
## `hd report committers`
|
|
78
78
|
|
|
@@ -85,7 +85,7 @@ USAGE
|
|
|
85
85
|
FLAGS
|
|
86
86
|
-c, --csv Output in CSV format
|
|
87
87
|
-m, --months=<value> [default: 12] The number of months of git history to review
|
|
88
|
-
-s, --save Save the committers report as
|
|
88
|
+
-s, --save Save the committers report as herodevs.committers.<output>
|
|
89
89
|
|
|
90
90
|
GLOBAL FLAGS
|
|
91
91
|
--json Format output as json.
|
|
@@ -103,7 +103,7 @@ EXAMPLES
|
|
|
103
103
|
$ hd report committers --csv
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
106
|
+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/report/committers.ts)_
|
|
107
107
|
|
|
108
108
|
## `hd report purls`
|
|
109
109
|
|
|
@@ -117,7 +117,7 @@ FLAGS
|
|
|
117
117
|
-c, --csv Save output in CSV format (only applies when using --save)
|
|
118
118
|
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
119
119
|
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
120
|
-
-s, --save Save the list of purls as
|
|
120
|
+
-s, --save Save the list of purls as herodevs.purls.<output>
|
|
121
121
|
|
|
122
122
|
GLOBAL FLAGS
|
|
123
123
|
--json Format output as json.
|
|
@@ -137,7 +137,7 @@ EXAMPLES
|
|
|
137
137
|
$ hd report purls --save --csv
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
140
|
+
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/report/purls.ts)_
|
|
141
141
|
|
|
142
142
|
## `hd scan eol`
|
|
143
143
|
|
|
@@ -145,15 +145,13 @@ Scan a given sbom for EOL data
|
|
|
145
145
|
|
|
146
146
|
```
|
|
147
147
|
USAGE
|
|
148
|
-
$ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s]
|
|
148
|
+
$ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s]
|
|
149
149
|
|
|
150
150
|
FLAGS
|
|
151
|
-
-a, --all Show all components (default is EOL and SUPPORTED only)
|
|
152
151
|
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
153
152
|
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
154
153
|
-p, --purls=<value> The file path of a list of purls to scan for EOL
|
|
155
|
-
-s, --save Save the generated report as
|
|
156
|
-
-t, --table Display the results in a table
|
|
154
|
+
-s, --save Save the generated report as herodevs.report.json in the scanned directory
|
|
157
155
|
|
|
158
156
|
GLOBAL FLAGS
|
|
159
157
|
--json Format output as json.
|
|
@@ -171,7 +169,7 @@ EXAMPLES
|
|
|
171
169
|
$ hd scan eol -a --dir=./my-project
|
|
172
170
|
```
|
|
173
171
|
|
|
174
|
-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
172
|
+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/scan/eol.ts)_
|
|
175
173
|
|
|
176
174
|
## `hd scan sbom`
|
|
177
175
|
|
|
@@ -185,7 +183,7 @@ FLAGS
|
|
|
185
183
|
-b, --background Run the scan in the background
|
|
186
184
|
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
187
185
|
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
188
|
-
-s, --save Save the generated SBOM as
|
|
186
|
+
-s, --save Save the generated SBOM as herodevs.sbom.json in the scanned directory
|
|
189
187
|
|
|
190
188
|
GLOBAL FLAGS
|
|
191
189
|
--json Format output as json.
|
|
@@ -199,7 +197,7 @@ EXAMPLES
|
|
|
199
197
|
$ hd scan sbom --file=path/to/sbom.json
|
|
200
198
|
```
|
|
201
199
|
|
|
202
|
-
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.
|
|
200
|
+
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.4/src/commands/scan/sbom.ts)_
|
|
203
201
|
|
|
204
202
|
## `hd update [CHANNEL]`
|
|
205
203
|
|
|
@@ -237,5 +235,5 @@ EXAMPLES
|
|
|
237
235
|
$ hd update --available
|
|
238
236
|
```
|
|
239
237
|
|
|
240
|
-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.
|
|
238
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.45/src/commands/update.ts)_
|
|
241
239
|
<!-- commandsstop -->
|
package/bin/main.js
CHANGED
|
@@ -7,9 +7,9 @@ async function main(isProduction = false) {
|
|
|
7
7
|
strict: false, // Don't validate flags
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
// If no arguments at all, default to scan:eol
|
|
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('-')) {
|
|
@@ -8,13 +8,12 @@ export interface ScanInputOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export declare const DEFAULT_SCAN_BATCH_SIZE = 1000;
|
|
10
10
|
export declare const DEFAULT_SCAN_INPUT_OPTIONS: ScanInputOptions;
|
|
11
|
-
export type ScanResultComponentsMap = Map<string, InsightsEolScanComponent>;
|
|
12
11
|
export type ScanInput = {
|
|
13
12
|
components: string[];
|
|
14
13
|
options: ScanInputOptions;
|
|
15
14
|
};
|
|
16
15
|
export interface ScanResult {
|
|
17
|
-
components:
|
|
16
|
+
components: Map<string, InsightsEolScanComponent>;
|
|
18
17
|
createdOn?: string;
|
|
19
18
|
diagnostics?: Record<string, unknown>;
|
|
20
19
|
message: string;
|
|
@@ -38,10 +38,14 @@ export interface InsightsEolScanComponentInfo {
|
|
|
38
38
|
status: ComponentStatus;
|
|
39
39
|
daysEol: number | null;
|
|
40
40
|
vulnCount: number | null;
|
|
41
|
+
nesAvailable?: boolean;
|
|
41
42
|
}
|
|
42
43
|
export interface InsightsEolScanComponent {
|
|
43
44
|
info: InsightsEolScanComponentInfo;
|
|
44
45
|
purl: string;
|
|
46
|
+
remediation?: {
|
|
47
|
+
id: string;
|
|
48
|
+
} | null;
|
|
45
49
|
}
|
|
46
50
|
export interface ScanWarning {
|
|
47
51
|
purl: string;
|
|
@@ -51,4 +55,4 @@ export interface ScanWarning {
|
|
|
51
55
|
diagnostics?: Record<string, unknown>;
|
|
52
56
|
}
|
|
53
57
|
export type ComponentStatus = (typeof VALID_STATUSES)[number];
|
|
54
|
-
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "
|
|
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,8 +9,6 @@ export default class ScanEol extends Command {
|
|
|
9
9
|
purls: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
-
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
-
table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
12
|
};
|
|
15
13
|
run(): Promise<{
|
|
16
14
|
components: InsightsEolScanComponent[];
|
|
@@ -20,13 +18,7 @@ export default class ScanEol extends Command {
|
|
|
20
18
|
private getPurlsFromFile;
|
|
21
19
|
private printWebReportUrl;
|
|
22
20
|
private scanSbom;
|
|
23
|
-
private getFilteredComponents;
|
|
24
21
|
private saveReport;
|
|
25
22
|
private displayResults;
|
|
26
|
-
private displayResultsInTable;
|
|
27
|
-
private displayTable;
|
|
28
|
-
private displayNoComponentsMessage;
|
|
29
|
-
private logLine;
|
|
30
|
-
private displayStatusSection;
|
|
31
|
-
private logLegend;
|
|
32
23
|
}
|
|
24
|
+
export declare function countComponentsByStatus(components: InsightsEolScanComponent[]): Record<ComponentStatus | 'NES_AVAILABLE', number>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command, Flags, ux } from '@oclif/core';
|
|
4
|
+
import terminalLink from 'terminal-link';
|
|
4
5
|
import { batchSubmitPurls } from "../../api/nes/nes.client.js";
|
|
5
|
-
import { config } from "../../config/constants.js";
|
|
6
|
+
import { config, filenamePrefix } from "../../config/constants.js";
|
|
6
7
|
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
7
8
|
import { extractPurls, parsePurlsFile } from "../../service/purls.svc.js";
|
|
8
|
-
import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from "../../ui/eol.ui.js";
|
|
9
9
|
import { INDICATORS, SCAN_ID_KEY, STATUS_COLORS } from "../../ui/shared.ui.js";
|
|
10
10
|
import ScanSbom from "./sbom.js";
|
|
11
11
|
export default class ScanEol extends Command {
|
|
@@ -33,38 +33,25 @@ export default class ScanEol extends Command {
|
|
|
33
33
|
save: Flags.boolean({
|
|
34
34
|
char: 's',
|
|
35
35
|
default: false,
|
|
36
|
-
description:
|
|
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
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
56
|
return { components, createdOn: scan.createdOn ?? '' };
|
|
70
57
|
}
|
|
@@ -87,11 +74,11 @@ export default class ScanEol extends Command {
|
|
|
87
74
|
}
|
|
88
75
|
}
|
|
89
76
|
printWebReportUrl(scanId) {
|
|
90
|
-
this.
|
|
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,91 +95,59 @@ export default class ScanEol extends Command {
|
|
|
108
95
|
catch (error) {
|
|
109
96
|
this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
|
|
110
97
|
}
|
|
111
|
-
if (scan.components.size === 0) {
|
|
112
|
-
this.warn('No components found in scan');
|
|
113
|
-
}
|
|
114
98
|
return scan;
|
|
115
99
|
}
|
|
116
|
-
getFilteredComponents(scan, all) {
|
|
117
|
-
return Array.from(scan.components.values()).filter((component) => all || ['EOL', 'SUPPORTED'].includes(component.info.status));
|
|
118
|
-
}
|
|
119
100
|
async saveReport(components, createdOn) {
|
|
120
101
|
const { flags } = await this.parse(ScanEol);
|
|
121
|
-
const reportPath = path.join(flags.dir || process.cwd(),
|
|
102
|
+
const reportPath = path.join(flags.dir || process.cwd(), `${filenamePrefix}.report.json`);
|
|
122
103
|
try {
|
|
123
104
|
fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2));
|
|
124
|
-
this.log(
|
|
105
|
+
this.log(`Report saved to ${filenamePrefix}.report.json`);
|
|
125
106
|
}
|
|
126
107
|
catch (error) {
|
|
127
108
|
if (!isErrnoException(error)) {
|
|
128
109
|
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
129
110
|
}
|
|
130
111
|
if (error.code === 'EACCES') {
|
|
131
|
-
this.error(
|
|
112
|
+
this.error(`Permission denied. Unable to save report to ${filenamePrefix}.report.json`);
|
|
132
113
|
}
|
|
133
114
|
else if (error.code === 'ENOSPC') {
|
|
134
|
-
this.error(
|
|
115
|
+
this.error(`No space left on device. Unable to save report to ${filenamePrefix}.report.json`);
|
|
135
116
|
}
|
|
136
117
|
else {
|
|
137
118
|
this.error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
138
119
|
}
|
|
139
120
|
}
|
|
140
121
|
}
|
|
141
|
-
displayResults(
|
|
142
|
-
const { UNKNOWN, OK,
|
|
143
|
-
if (!UNKNOWN
|
|
144
|
-
this.displayNoComponentsMessage(all);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
this.log(ux.colorize('bold', 'Here are the results of the scan:'));
|
|
148
|
-
this.logLine();
|
|
149
|
-
// Display sections in order of increasing severity
|
|
150
|
-
for (const components of [UNKNOWN, OK, SUPPORTED, EOL]) {
|
|
151
|
-
this.displayStatusSection(components);
|
|
152
|
-
}
|
|
153
|
-
this.logLegend();
|
|
154
|
-
}
|
|
155
|
-
displayResultsInTable(scan, all) {
|
|
156
|
-
const grouped = groupComponentsByStatus(scan.components);
|
|
157
|
-
const statuses = ['SUPPORTED', 'EOL'];
|
|
158
|
-
if (all) {
|
|
159
|
-
statuses.unshift('UNKNOWN', 'OK');
|
|
160
|
-
}
|
|
161
|
-
for (const status of statuses) {
|
|
162
|
-
const components = grouped[status];
|
|
163
|
-
if (components.length > 0) {
|
|
164
|
-
const table = createTableForStatus(grouped, status);
|
|
165
|
-
this.displayTable(table, components.length, status);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
this.logLegend();
|
|
169
|
-
}
|
|
170
|
-
displayTable(table, count, status) {
|
|
171
|
-
this.log(ux.colorize(STATUS_COLORS[status], `${INDICATORS[status]} ${count} ${status} Component(s):`));
|
|
172
|
-
this.log(ux.colorize(STATUS_COLORS[status], table));
|
|
173
|
-
}
|
|
174
|
-
displayNoComponentsMessage(all) {
|
|
175
|
-
if (!all) {
|
|
176
|
-
this.log(ux.colorize('yellow', 'No End-of-Life or Supported components found in scan.'));
|
|
177
|
-
this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
122
|
+
displayResults(components) {
|
|
123
|
+
const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(components);
|
|
124
|
+
if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
|
|
180
125
|
this.log(ux.colorize('yellow', 'No components found in scan.'));
|
|
126
|
+
return;
|
|
181
127
|
}
|
|
128
|
+
this.log(ux.colorize('bold', 'Scan results:'));
|
|
129
|
+
this.log(ux.colorize('bold', '-'.repeat(40)));
|
|
130
|
+
this.log(ux.colorize('bold', `${components.length.toLocaleString()} total packages scanned`));
|
|
131
|
+
this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} ${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`));
|
|
132
|
+
this.log(ux.colorize(STATUS_COLORS.EOL_UPCOMING, `${INDICATORS.EOL_UPCOMING}${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`));
|
|
133
|
+
this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} ${OK.toLocaleString().padEnd(5)} OK`));
|
|
134
|
+
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} ${UNKNOWN.toLocaleString().padEnd(5)} Unknown Status`));
|
|
135
|
+
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} ${NES_AVAILABLE.toLocaleString().padEnd(5)} HeroDevs NES Remediation${NES_AVAILABLE !== 1 ? 's' : ''} Available`));
|
|
182
136
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
137
|
+
}
|
|
138
|
+
export function countComponentsByStatus(components) {
|
|
139
|
+
const grouped = {
|
|
140
|
+
UNKNOWN: 0,
|
|
141
|
+
OK: 0,
|
|
142
|
+
EOL_UPCOMING: 0,
|
|
143
|
+
EOL: 0,
|
|
144
|
+
NES_AVAILABLE: 0,
|
|
145
|
+
};
|
|
146
|
+
for (const component of components) {
|
|
147
|
+
grouped[component.info.status]++;
|
|
148
|
+
if (component.info.nesAvailable) {
|
|
149
|
+
grouped.NES_AVAILABLE++;
|
|
190
150
|
}
|
|
191
151
|
}
|
|
192
|
-
|
|
193
|
-
this.log(ux.colorize(STATUS_COLORS.UNKNOWN, `${INDICATORS.UNKNOWN} = No Known Issues`));
|
|
194
|
-
this.log(ux.colorize(STATUS_COLORS.OK, `${INDICATORS.OK} = OK`));
|
|
195
|
-
this.log(ux.colorize(STATUS_COLORS.SUPPORTED, `${INDICATORS.SUPPORTED}= Supported: End-of-Life (EOL) is scheduled`));
|
|
196
|
-
this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} = End of Life (EOL)`));
|
|
197
|
-
}
|
|
152
|
+
return grouped;
|
|
198
153
|
}
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { Command, Flags, ux } from '@oclif/core';
|
|
5
|
+
import { filenamePrefix } from "../../config/constants.js";
|
|
5
6
|
import { createSbom, validateIsCycloneDxSbom } from "../../service/eol/eol.svc.js";
|
|
6
7
|
import { getErrorMessage } from "../../service/error.svc.js";
|
|
7
8
|
export default class ScanSbom extends Command {
|
|
@@ -23,7 +24,7 @@ export default class ScanSbom extends Command {
|
|
|
23
24
|
save: Flags.boolean({
|
|
24
25
|
char: 's',
|
|
25
26
|
default: false,
|
|
26
|
-
description:
|
|
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,
|
|
@@ -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.2.
|
|
43
|
-
"@oclif/plugin-update": "^4.6.
|
|
44
|
-
"@oclif/table": "^0.4.8",
|
|
40
|
+
"@cyclonedx/cdxgen": "^11.4.1",
|
|
41
|
+
"@oclif/core": "^4.4.0",
|
|
42
|
+
"@oclif/plugin-help": "^6.2.29",
|
|
43
|
+
"@oclif/plugin-update": "^4.6.45",
|
|
45
44
|
"graphql": "^16.11.0",
|
|
46
45
|
"packageurl-js": "^2.0.1",
|
|
46
|
+
"terminal-link": "^4.0.0",
|
|
47
47
|
"update-notifier": "^7.3.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@biomejs/biome": "^1.9.4",
|
|
51
51
|
"@oclif/test": "^4.1.13",
|
|
52
52
|
"@types/inquirer": "^9.0.8",
|
|
53
|
-
"@types/node": "^22.15.
|
|
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
60
|
"ts-node": "^10.9.2",
|
|
61
|
-
"tsx": "^4.
|
|
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
|
-
}
|