@herodevs/cli 1.2.0-beta.1 → 1.4.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 +48 -6
- package/dist/api/nes/nes.client.d.ts +10 -7
- package/dist/api/nes/nes.client.js +81 -5
- package/dist/api/types/hd-cli.types.d.ts +29 -0
- package/dist/api/types/hd-cli.types.js +10 -0
- package/dist/api/types/nes.types.d.ts +34 -31
- package/dist/api/types/nes.types.js +0 -10
- package/dist/commands/report/purls.js +1 -3
- package/dist/commands/scan/eol.d.ts +5 -2
- package/dist/commands/scan/eol.js +37 -6
- package/dist/service/eol/cdx.svc.d.ts +2 -8
- package/dist/service/eol/cdx.svc.js +2 -19
- package/dist/service/eol/eol.svc.d.ts +1 -2
- package/dist/service/eol/eol.svc.js +0 -1
- package/dist/service/eol/sbom.worker.js +0 -1
- package/dist/service/nes/nes.svc.d.ts +4 -3
- package/dist/service/nes/nes.svc.js +4 -4
- package/dist/service/purls.svc.d.ts +6 -0
- package/dist/service/purls.svc.js +26 -0
- package/dist/ui/eol.ui.d.ts +2 -1
- package/package.json +23 -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.4.0-beta.1 linux-x64 node-v22.14.0
|
|
20
20
|
$ hd --help [COMMAND]
|
|
21
21
|
USAGE
|
|
22
22
|
$ hd COMMAND
|
|
@@ -30,6 +30,7 @@ USAGE
|
|
|
30
30
|
* [`hd report purls`](#hd-report-purls)
|
|
31
31
|
* [`hd scan eol`](#hd-scan-eol)
|
|
32
32
|
* [`hd scan sbom`](#hd-scan-sbom)
|
|
33
|
+
* [`hd update [CHANNEL]`](#hd-update-channel)
|
|
33
34
|
|
|
34
35
|
## `hd help [COMMAND]`
|
|
35
36
|
|
|
@@ -80,7 +81,7 @@ EXAMPLES
|
|
|
80
81
|
$ hd report committers --csv
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
_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.4.0-beta.1/src/commands/report/committers.ts)_
|
|
84
85
|
|
|
85
86
|
## `hd report purls`
|
|
86
87
|
|
|
@@ -114,7 +115,7 @@ EXAMPLES
|
|
|
114
115
|
$ hd report purls --save --csv
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
_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.4.0-beta.1/src/commands/report/purls.ts)_
|
|
118
119
|
|
|
119
120
|
## `hd scan eol`
|
|
120
121
|
|
|
@@ -122,13 +123,14 @@ Scan a given sbom for EOL data
|
|
|
122
123
|
|
|
123
124
|
```
|
|
124
125
|
USAGE
|
|
125
|
-
$ hd scan eol [--json] [-f <value>] [-d <value>] [-s] [-a] [-c]
|
|
126
|
+
$ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-c]
|
|
126
127
|
|
|
127
128
|
FLAGS
|
|
128
129
|
-a, --all Show all components (default is EOL and LTS only)
|
|
129
130
|
-c, --getCustomerSupport Get Never-Ending Support for End-of-Life components
|
|
130
131
|
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
131
132
|
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
133
|
+
-p, --purls=<value> The file path of a list of purls to scan for EOL
|
|
132
134
|
-s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
|
|
133
135
|
|
|
134
136
|
GLOBAL FLAGS
|
|
@@ -142,10 +144,12 @@ EXAMPLES
|
|
|
142
144
|
|
|
143
145
|
$ hd scan eol --file=path/to/sbom.json
|
|
144
146
|
|
|
147
|
+
$ hd scan eol --purls=path/to/purls.json
|
|
148
|
+
|
|
145
149
|
$ hd scan eol -a --dir=./my-project
|
|
146
150
|
```
|
|
147
151
|
|
|
148
|
-
_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.4.0-beta.1/src/commands/scan/eol.ts)_
|
|
149
153
|
|
|
150
154
|
## `hd scan sbom`
|
|
151
155
|
|
|
@@ -173,5 +177,43 @@ EXAMPLES
|
|
|
173
177
|
$ hd scan sbom --file=path/to/sbom.json
|
|
174
178
|
```
|
|
175
179
|
|
|
176
|
-
_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.4.0-beta.1/src/commands/scan/sbom.ts)_
|
|
181
|
+
|
|
182
|
+
## `hd update [CHANNEL]`
|
|
183
|
+
|
|
184
|
+
update the hd CLI
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
USAGE
|
|
188
|
+
$ hd update [CHANNEL] [--force | | [-a | -v <value> | -i]] [-b ]
|
|
189
|
+
|
|
190
|
+
FLAGS
|
|
191
|
+
-a, --available See available versions.
|
|
192
|
+
-b, --verbose Show more details about the available versions.
|
|
193
|
+
-i, --interactive Interactively select version to install. This is ignored if a channel is provided.
|
|
194
|
+
-v, --version=<value> Install a specific version.
|
|
195
|
+
--force Force a re-download of the requested version.
|
|
196
|
+
|
|
197
|
+
DESCRIPTION
|
|
198
|
+
update the hd CLI
|
|
199
|
+
|
|
200
|
+
EXAMPLES
|
|
201
|
+
Update to the stable channel:
|
|
202
|
+
|
|
203
|
+
$ hd update stable
|
|
204
|
+
|
|
205
|
+
Update to a specific version:
|
|
206
|
+
|
|
207
|
+
$ hd update --version 1.0.0
|
|
208
|
+
|
|
209
|
+
Interactively select version:
|
|
210
|
+
|
|
211
|
+
$ hd update --interactive
|
|
212
|
+
|
|
213
|
+
See available versions:
|
|
214
|
+
|
|
215
|
+
$ hd update --available
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.6.36/src/commands/update.ts)_
|
|
177
219
|
<!-- commandsstop -->
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import type * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { InsightsEolScanInput, InsightsEolScanResult } from '../../api/types/nes.types.ts';
|
|
3
|
+
import type { ProcessBatchOptions, ScanInputOptions, ScanResult } from '../types/hd-cli.types.ts';
|
|
3
4
|
export interface NesClient {
|
|
4
5
|
scan: {
|
|
5
|
-
|
|
6
|
+
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
export declare class NesApolloClient implements NesClient {
|
|
9
10
|
#private;
|
|
10
11
|
scan: {
|
|
11
|
-
|
|
12
|
+
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
12
13
|
};
|
|
13
14
|
constructor(url: string);
|
|
14
15
|
mutate<T, V extends Record<string, unknown>>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
15
16
|
query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export declare
|
|
18
|
+
export declare const batchSubmitPurls: (purls: string[], options: ScanInputOptions, batchSize: number) => Promise<ScanResult>;
|
|
19
|
+
export declare const createBatches: (items: string[], batchSize: number) => string[][];
|
|
20
|
+
export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
|
|
21
|
+
export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
|
|
22
|
+
export declare const handleBatchResults: (results: InsightsEolScanResult[]) => ScanResult;
|
|
23
|
+
export declare const buildInsightsEolScanInput: (purls: string[], options: ScanInputOptions) => InsightsEolScanInput;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ApolloClient } from "../../api/client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { debugLogger } from "../../service/log.svc.js";
|
|
3
|
+
import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
|
|
3
4
|
export class NesApolloClient {
|
|
4
5
|
scan = {
|
|
5
|
-
|
|
6
|
+
purls: SbomScanner(this),
|
|
6
7
|
};
|
|
7
8
|
#apollo;
|
|
8
9
|
constructor(url) {
|
|
@@ -16,13 +17,88 @@ export class NesApolloClient {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
+
* Submit a scan for a list of purls after they've been batched by batchSubmitPurls
|
|
20
21
|
*/
|
|
21
|
-
|
|
22
|
+
function submitScan(purls, options) {
|
|
22
23
|
// NOTE: GRAPHQL_HOST is set in `./bin/dev.js` or tests
|
|
23
24
|
const host = process.env.GRAPHQL_HOST || 'https://api.nes.herodevs.com';
|
|
24
25
|
const path = process.env.GRAPHQL_PATH || '/graphql';
|
|
25
26
|
const url = host + path;
|
|
26
27
|
const client = new NesApolloClient(url);
|
|
27
|
-
return client.scan.
|
|
28
|
+
return client.scan.purls(purls, options);
|
|
28
29
|
}
|
|
30
|
+
export const batchSubmitPurls = async (purls, options, batchSize) => {
|
|
31
|
+
try {
|
|
32
|
+
const batches = createBatches(purls, batchSize);
|
|
33
|
+
debugLogger('Processing %d batches', batches.length);
|
|
34
|
+
if (batches.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
components: new Map(),
|
|
37
|
+
message: 'No batches to process',
|
|
38
|
+
success: true,
|
|
39
|
+
warnings: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const results = await processBatches(batches, options);
|
|
43
|
+
return handleBatchResults(results);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
debugLogger('Fatal error in batchSubmitPurls: %s', error);
|
|
47
|
+
throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const createBatches = (items, batchSize) => {
|
|
51
|
+
const numberOfBatches = Math.ceil(items.length / batchSize);
|
|
52
|
+
return Array.from({ length: numberOfBatches }, (_, index) => {
|
|
53
|
+
const startIndex = index * batchSize;
|
|
54
|
+
const endIndex = startIndex + batchSize;
|
|
55
|
+
return items.slice(startIndex, endIndex);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
export const processBatch = async ({ batch, index, totalPages, scanOptions, previousScanId, }) => {
|
|
59
|
+
const page = index + 1;
|
|
60
|
+
if (page > totalPages) {
|
|
61
|
+
throw new Error('Total pages exceeded');
|
|
62
|
+
}
|
|
63
|
+
debugLogger('Processing batch %d of %d', page, totalPages);
|
|
64
|
+
const result = await submitScan(batch, {
|
|
65
|
+
...scanOptions,
|
|
66
|
+
page,
|
|
67
|
+
totalPages,
|
|
68
|
+
scanId: previousScanId,
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
};
|
|
72
|
+
export const processBatches = async (batches, scanOptions) => {
|
|
73
|
+
const totalPages = batches.length;
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const [index, batch] of batches.entries()) {
|
|
76
|
+
const previousScanId = results[index - 1]?.scanId;
|
|
77
|
+
const result = await processBatch({
|
|
78
|
+
batch,
|
|
79
|
+
index,
|
|
80
|
+
totalPages,
|
|
81
|
+
scanOptions,
|
|
82
|
+
previousScanId,
|
|
83
|
+
});
|
|
84
|
+
results.push(result);
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
};
|
|
88
|
+
export const handleBatchResults = (results) => {
|
|
89
|
+
if (results.length === 0) {
|
|
90
|
+
throw new Error('No results to process');
|
|
91
|
+
}
|
|
92
|
+
// The API returns placeholders for each batch except the last one.
|
|
93
|
+
const finalResult = results[results.length - 1];
|
|
94
|
+
return buildScanResult(finalResult);
|
|
95
|
+
};
|
|
96
|
+
export const buildInsightsEolScanInput = (purls, options) => {
|
|
97
|
+
const { type, page, totalPages } = options;
|
|
98
|
+
return {
|
|
99
|
+
components: purls,
|
|
100
|
+
type,
|
|
101
|
+
page,
|
|
102
|
+
totalPages,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ComponentStatus, type InsightsEolScanComponent, type ScanWarning } from './nes.types.ts';
|
|
2
|
+
export declare const isValidComponentStatus: (status: string) => status is ComponentStatus;
|
|
3
|
+
export interface ScanInputOptions {
|
|
4
|
+
type: 'SBOM' | 'OTHER';
|
|
5
|
+
page: number;
|
|
6
|
+
totalPages: number;
|
|
7
|
+
scanId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_SCAN_BATCH_SIZE = 1000;
|
|
10
|
+
export declare const DEFAULT_SCAN_INPUT_OPTIONS: ScanInputOptions;
|
|
11
|
+
export type ScanResultComponentsMap = Map<string, InsightsEolScanComponent>;
|
|
12
|
+
export type ScanInput = {
|
|
13
|
+
components: string[];
|
|
14
|
+
options: ScanInputOptions;
|
|
15
|
+
};
|
|
16
|
+
export interface ScanResult {
|
|
17
|
+
components: ScanResultComponentsMap;
|
|
18
|
+
diagnostics?: Record<string, unknown>;
|
|
19
|
+
message: string;
|
|
20
|
+
success: boolean;
|
|
21
|
+
warnings: ScanWarning[];
|
|
22
|
+
}
|
|
23
|
+
export interface ProcessBatchOptions {
|
|
24
|
+
batch: string[];
|
|
25
|
+
index: number;
|
|
26
|
+
totalPages: number;
|
|
27
|
+
scanOptions: ScanInputOptions;
|
|
28
|
+
previousScanId?: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { VALID_STATUSES } from "./nes.types.js";
|
|
2
|
+
export const isValidComponentStatus = (status) => {
|
|
3
|
+
return VALID_STATUSES.includes(status);
|
|
4
|
+
};
|
|
5
|
+
export const DEFAULT_SCAN_BATCH_SIZE = 1000;
|
|
6
|
+
export const DEFAULT_SCAN_INPUT_OPTIONS = {
|
|
7
|
+
type: 'SBOM',
|
|
8
|
+
page: 1,
|
|
9
|
+
totalPages: 1,
|
|
10
|
+
};
|
|
@@ -1,35 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Input parameters for the EOL scan operation
|
|
3
|
+
*/
|
|
4
|
+
export interface InsightsEolScanInput {
|
|
5
|
+
scanId?: string;
|
|
6
|
+
/** Array of package URLs in purl format to scan */
|
|
2
7
|
components: string[];
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
/** The type of scan being performed (e.g. 'SBOM') */
|
|
9
|
+
type: string;
|
|
10
|
+
page: number;
|
|
11
|
+
totalPages: number;
|
|
12
|
+
}
|
|
7
13
|
export interface ScanResponse {
|
|
8
14
|
insights: {
|
|
9
15
|
scan: {
|
|
10
|
-
eol:
|
|
16
|
+
eol: InsightsEolScanResult;
|
|
11
17
|
};
|
|
12
18
|
};
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Result of the EOL scan operation
|
|
22
|
+
*/
|
|
23
|
+
export interface InsightsEolScanResult {
|
|
24
|
+
scanId?: string;
|
|
18
25
|
success: boolean;
|
|
19
|
-
|
|
26
|
+
message: string;
|
|
27
|
+
components: InsightsEolScanComponent[];
|
|
28
|
+
warnings: ScanWarning[];
|
|
20
29
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Information about a component's EOL status
|
|
32
|
+
*/
|
|
33
|
+
export interface InsightsEolScanComponentInfo {
|
|
34
|
+
isEol: boolean;
|
|
35
|
+
isUnsafe: boolean;
|
|
36
|
+
eolAt: Date | null;
|
|
37
|
+
status: ComponentStatus;
|
|
38
|
+
daysEol: number | null;
|
|
39
|
+
}
|
|
40
|
+
export interface InsightsEolScanComponent {
|
|
41
|
+
info: InsightsEolScanComponentInfo;
|
|
33
42
|
purl: string;
|
|
34
43
|
}
|
|
35
44
|
export interface ScanWarning {
|
|
@@ -39,11 +48,5 @@ export interface ScanWarning {
|
|
|
39
48
|
error?: unknown;
|
|
40
49
|
diagnostics?: Record<string, unknown>;
|
|
41
50
|
}
|
|
42
|
-
export type
|
|
43
|
-
export
|
|
44
|
-
components: ScanResultComponentsMap;
|
|
45
|
-
diagnostics?: Record<string, unknown>;
|
|
46
|
-
message: string;
|
|
47
|
-
success: boolean;
|
|
48
|
-
warnings: ScanWarning[];
|
|
49
|
-
}
|
|
51
|
+
export type ComponentStatus = (typeof VALID_STATUSES)[number];
|
|
52
|
+
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "LTS"];
|
|
@@ -1,11 +1 @@
|
|
|
1
1
|
export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'LTS'];
|
|
2
|
-
export const isValidComponentStatus = (status) => {
|
|
3
|
-
return VALID_STATUSES.includes(status);
|
|
4
|
-
};
|
|
5
|
-
export const validateComponentStatuses = (statuses) => {
|
|
6
|
-
const validStatuses = statuses.filter(isValidComponentStatus);
|
|
7
|
-
if (validStatuses.length !== statuses.length) {
|
|
8
|
-
throw new Error('Invalid component status provided');
|
|
9
|
-
}
|
|
10
|
-
return validStatuses;
|
|
11
|
-
};
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { 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;
|
|
6
6
|
static examples: string[];
|
|
7
7
|
static flags: {
|
|
8
8
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
purls: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
getCustomerSupport: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
};
|
|
14
15
|
run(): Promise<{
|
|
15
|
-
components:
|
|
16
|
+
components: InsightsEolScanComponent[];
|
|
16
17
|
}>;
|
|
18
|
+
private getScan;
|
|
19
|
+
private getPurlsFromFile;
|
|
17
20
|
private scanSbom;
|
|
18
21
|
private getFilteredComponents;
|
|
19
22
|
private saveReport;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { Command, Flags, ux } from '@oclif/core';
|
|
3
|
-
import {
|
|
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';
|
|
4
6
|
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
5
7
|
import { extractPurls } from "../../service/purls.svc.js";
|
|
8
|
+
import { parsePurlsFile } from "../../service/purls.svc.js";
|
|
6
9
|
import { createStatusDisplay } from "../../ui/eol.ui.js";
|
|
7
10
|
import { INDICATORS, STATUS_COLORS } from "../../ui/shared.us.js";
|
|
8
11
|
import ScanSbom from "./sbom.js";
|
|
@@ -12,6 +15,7 @@ export default class ScanEol extends Command {
|
|
|
12
15
|
static examples = [
|
|
13
16
|
'<%= config.bin %> <%= command.id %> --dir=./my-project',
|
|
14
17
|
'<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --purls=path/to/purls.json',
|
|
15
19
|
'<%= config.bin %> <%= command.id %> -a --dir=./my-project',
|
|
16
20
|
];
|
|
17
21
|
static flags = {
|
|
@@ -19,6 +23,10 @@ export default class ScanEol extends Command {
|
|
|
19
23
|
char: 'f',
|
|
20
24
|
description: 'The file path of an existing cyclonedx sbom to scan for EOL',
|
|
21
25
|
}),
|
|
26
|
+
purls: Flags.string({
|
|
27
|
+
char: 'p',
|
|
28
|
+
description: 'The file path of a list of purls to scan for EOL',
|
|
29
|
+
}),
|
|
22
30
|
dir: Flags.string({
|
|
23
31
|
char: 'd',
|
|
24
32
|
description: 'The directory to scan in order to create a cyclonedx sbom',
|
|
@@ -44,8 +52,7 @@ export default class ScanEol extends Command {
|
|
|
44
52
|
if (flags.getCustomerSupport) {
|
|
45
53
|
this.log(ux.colorize('yellow', 'Never-Ending Support is on the way. Please stay tuned for this feature.'));
|
|
46
54
|
}
|
|
47
|
-
const
|
|
48
|
-
const scan = await this.scanSbom(sbom);
|
|
55
|
+
const scan = await this.getScan(flags, this.config);
|
|
49
56
|
ux.action.stop('\nScan completed');
|
|
50
57
|
const filteredComponents = this.getFilteredComponents(scan, flags.all);
|
|
51
58
|
if (flags.save) {
|
|
@@ -57,7 +64,29 @@ export default class ScanEol extends Command {
|
|
|
57
64
|
await this.displayResults(scan, flags.all);
|
|
58
65
|
return { components: filteredComponents };
|
|
59
66
|
}
|
|
60
|
-
async
|
|
67
|
+
async getScan(flags, config) {
|
|
68
|
+
if (flags.purls) {
|
|
69
|
+
ux.action.start(`Scanning purls from ${flags.purls}`);
|
|
70
|
+
const purls = this.getPurlsFromFile(flags.purls);
|
|
71
|
+
return batchSubmitPurls(purls, DEFAULT_SCAN_INPUT_OPTIONS, DEFAULT_SCAN_BATCH_SIZE);
|
|
72
|
+
}
|
|
73
|
+
const sbom = await ScanSbom.loadSbom(flags, config);
|
|
74
|
+
const scan = this.scanSbom(sbom, flags);
|
|
75
|
+
return scan;
|
|
76
|
+
}
|
|
77
|
+
getPurlsFromFile(filePath) {
|
|
78
|
+
if (typeof filePath !== 'string') {
|
|
79
|
+
this.error(`Failed to parse file path: ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const purlsFileString = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
return parsePurlsFile(purlsFileString);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async scanSbom(sbom, flags) {
|
|
61
90
|
let scan;
|
|
62
91
|
let purls;
|
|
63
92
|
try {
|
|
@@ -67,7 +96,7 @@ export default class ScanEol extends Command {
|
|
|
67
96
|
this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
|
|
68
97
|
}
|
|
69
98
|
try {
|
|
70
|
-
scan = await
|
|
99
|
+
scan = await batchSubmitPurls(purls, DEFAULT_SCAN_INPUT_OPTIONS, DEFAULT_SCAN_BATCH_SIZE);
|
|
71
100
|
}
|
|
72
101
|
catch (error) {
|
|
73
102
|
this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
|
|
@@ -84,7 +113,9 @@ export default class ScanEol extends Command {
|
|
|
84
113
|
}
|
|
85
114
|
async saveReport(components) {
|
|
86
115
|
try {
|
|
87
|
-
|
|
116
|
+
const { flags } = await this.parse(ScanEol);
|
|
117
|
+
const reportPath = path.join(flags.dir || process.cwd(), 'nes.eol.json');
|
|
118
|
+
fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
|
|
88
119
|
this.log('Report saved to nes.eol.json');
|
|
89
120
|
}
|
|
90
121
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CdxGenOptions } from './eol.svc.ts';
|
|
2
2
|
export interface SbomEntry {
|
|
3
3
|
group: string;
|
|
4
4
|
name: string;
|
|
@@ -65,10 +65,4 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
65
65
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
66
66
|
* `directory`, and returns the `bomJson` property.
|
|
67
67
|
*/
|
|
68
|
-
export declare function createBomFromDir(directory: string, opts?: CdxGenOptions): Promise<
|
|
69
|
-
export declare const cdxgen: {
|
|
70
|
-
createBom: CdxCreator | undefined;
|
|
71
|
-
};
|
|
72
|
-
export declare function getCdxGen(): Promise<{
|
|
73
|
-
createBom: CdxCreator | undefined;
|
|
74
|
-
}>;
|
|
68
|
+
export declare function createBomFromDir(directory: string, opts?: CdxGenOptions): Promise<any>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createBom } from '@cyclonedx/cdxgen';
|
|
1
2
|
import { debugLogger } from "../../service/log.svc.js";
|
|
2
3
|
export const SBOM_DEFAULT__OPTIONS = {
|
|
3
4
|
$0: 'cdxgen',
|
|
@@ -58,25 +59,7 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
58
59
|
* `directory`, and returns the `bomJson` property.
|
|
59
60
|
*/
|
|
60
61
|
export async function createBomFromDir(directory, opts = {}) {
|
|
61
|
-
const
|
|
62
|
-
const sbom = await createBom?.(directory, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
62
|
+
const sbom = await createBom(directory, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
63
63
|
debugLogger('Successfully generated SBOM');
|
|
64
64
|
return sbom?.bomJson;
|
|
65
65
|
}
|
|
66
|
-
// use a value holder, for easier mocking
|
|
67
|
-
export const cdxgen = { createBom: undefined };
|
|
68
|
-
export async function getCdxGen() {
|
|
69
|
-
if (cdxgen.createBom) {
|
|
70
|
-
return cdxgen;
|
|
71
|
-
}
|
|
72
|
-
const ogEnv = process.env.NODE_ENV;
|
|
73
|
-
process.env.NODE_ENV = undefined;
|
|
74
|
-
try {
|
|
75
|
-
// @ts-expect-error
|
|
76
|
-
cdxgen.createBom = (await import('@cyclonedx/cdxgen')).createBom;
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
process.env.NODE_ENV = ogEnv;
|
|
80
|
-
}
|
|
81
|
-
return cdxgen;
|
|
82
|
-
}
|
|
@@ -8,6 +8,5 @@ export interface ScanOptions {
|
|
|
8
8
|
export type CdxCreator = (dir: string, opts: CdxGenOptions) => Promise<{
|
|
9
9
|
bomJson: Sbom;
|
|
10
10
|
}>;
|
|
11
|
-
export declare function createSbom(directory: string, opts?: ScanOptions): Promise<
|
|
11
|
+
export declare function createSbom(directory: string, opts?: ScanOptions): Promise<any>;
|
|
12
12
|
export declare function validateIsCycloneDxSbom(sbom: unknown): asserts sbom is Sbom;
|
|
13
|
-
export { cdxgen } from './cdx.svc.ts';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NesApolloClient } from '../../api/nes/nes.client.ts';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export declare const
|
|
2
|
+
import type { ScanInputOptions, ScanResult } from '../../api/types/hd-cli.types.ts';
|
|
3
|
+
import type { InsightsEolScanResult } from '../../api/types/nes.types.ts';
|
|
4
|
+
export declare const buildScanResult: (scan: InsightsEolScanResult) => ScanResult;
|
|
5
|
+
export declare const SbomScanner: (client: NesApolloClient) => (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
@@ -12,8 +12,9 @@ export const buildScanResult = (scan) => {
|
|
|
12
12
|
warnings: scan.warnings || [],
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
-
export const SbomScanner = (client) => async (purls) => {
|
|
16
|
-
const
|
|
15
|
+
export const SbomScanner = (client) => async (purls, options) => {
|
|
16
|
+
const { type, page, totalPages, scanId } = options;
|
|
17
|
+
const input = { components: purls, type, page, totalPages, scanId };
|
|
17
18
|
const res = await client.mutate(M_SCAN.gql, { input });
|
|
18
19
|
const scan = res.data?.insights?.scan?.eol;
|
|
19
20
|
if (!scan?.success) {
|
|
@@ -21,6 +22,5 @@ export const SbomScanner = (client) => async (purls) => {
|
|
|
21
22
|
debugLogger('scan failed');
|
|
22
23
|
throw new Error('Failed to provide scan: ');
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
return result;
|
|
25
|
+
return scan;
|
|
26
26
|
};
|
|
@@ -15,3 +15,9 @@ export declare function getPurlOutput(purls: string[], output: string): string;
|
|
|
15
15
|
* Translate an SBOM to a list of purls for api request.
|
|
16
16
|
*/
|
|
17
17
|
export declare function extractPurls(sbom: Sbom): Promise<string[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Parse a purls file in either JSON or text format, including the format of
|
|
20
|
+
* nes.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
|
|
21
|
+
* or a text file with one purl per line.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parsePurlsFile(purlsFileString: string): string[];
|
|
@@ -27,3 +27,29 @@ export async function extractPurls(sbom) {
|
|
|
27
27
|
const { components: comps } = sbom;
|
|
28
28
|
return comps.map((c) => c.purl) ?? [];
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse a purls file in either JSON or text format, including the format of
|
|
32
|
+
* nes.purls.json - { purls: [ 'pkg:npm/express@4.18.2', 'pkg:npm/react@18.3.1' ] }
|
|
33
|
+
* or a text file with one purl per line.
|
|
34
|
+
*/
|
|
35
|
+
export function parsePurlsFile(purlsFileString) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(purlsFileString);
|
|
38
|
+
if (parsed && Array.isArray(parsed.purls)) {
|
|
39
|
+
return parsed.purls;
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(parsed)) {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
const lines = purlsFileString
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((line) => line.trim())
|
|
49
|
+
.filter((line) => line.length > 0 && line.startsWith('pkg:'));
|
|
50
|
+
if (lines.length > 0) {
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Invalid purls file: must be either JSON with purls array or text file with one purl per line');
|
|
55
|
+
}
|
package/dist/ui/eol.ui.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScanResultComponentsMap } from '../api/types/hd-cli.types.ts';
|
|
2
|
+
import type { ComponentStatus } from '../api/types/nes.types.ts';
|
|
2
3
|
export declare function truncatePurl(purl: string): string;
|
|
3
4
|
export declare function colorizeStatus(status: ComponentStatus): string;
|
|
4
5
|
export declare function createStatusDisplay(components: ScanResultComponentsMap, all: boolean): Record<ComponentStatus, string[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-beta.1",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"clean": "shx rm -rf dist && npm run clean:files && shx rm -rf node_modules",
|
|
20
20
|
"clean:files": "shx rm -f nes.**.csv nes.**.json nes.**.text",
|
|
21
21
|
"dev": "npm run build && ./bin/dev.js",
|
|
22
|
-
"dev:debug": "npm run build && DEBUG
|
|
22
|
+
"dev:debug": "npm run build && DEBUG=oclif:* ./bin/dev.js",
|
|
23
23
|
"format": "biome format --write",
|
|
24
24
|
"lint": "biome lint --write",
|
|
25
25
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"readme": "npm run ci:fix && npm run build && npm exec oclif readme",
|
|
29
29
|
"test": "globstar -- node --import tsx --test \"test/**/*.test.ts\"",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
-
"version": "oclif readme && git add README.md"
|
|
31
|
+
"version": "oclif readme && git add README.md",
|
|
32
|
+
"test:e2e": "globstar -- node --import tsx --test \"e2e/**/*.test.ts\""
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"herodevs",
|
|
@@ -37,9 +38,10 @@
|
|
|
37
38
|
],
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@apollo/client": "^3.13.1",
|
|
40
|
-
"@cyclonedx/cdxgen": "^11.2.
|
|
41
|
+
"@cyclonedx/cdxgen": "^11.2.3",
|
|
41
42
|
"@oclif/core": "^4",
|
|
42
43
|
"@oclif/plugin-help": "^6",
|
|
44
|
+
"@oclif/plugin-update": "^4",
|
|
43
45
|
"graphql": "^16.8.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"typescript": "^5.8.0"
|
|
58
60
|
},
|
|
59
61
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
62
|
+
"node": ">=20.0.0"
|
|
61
63
|
},
|
|
62
64
|
"files": [
|
|
63
65
|
"./bin",
|
|
@@ -72,12 +74,26 @@
|
|
|
72
74
|
"commands": "./dist/commands",
|
|
73
75
|
"plugins": [
|
|
74
76
|
"@oclif/plugin-help",
|
|
75
|
-
"@oclif/plugin-plugins"
|
|
77
|
+
"@oclif/plugin-plugins",
|
|
78
|
+
"@oclif/plugin-update"
|
|
76
79
|
],
|
|
77
80
|
"hooks": {
|
|
78
81
|
"prerun": "./dist/hooks/prerun.js"
|
|
79
82
|
},
|
|
80
|
-
"topicSeparator": " "
|
|
83
|
+
"topicSeparator": " ",
|
|
84
|
+
"macos": {
|
|
85
|
+
"identifier": "com.herodevs.cli"
|
|
86
|
+
},
|
|
87
|
+
"win": {
|
|
88
|
+
"icon": "assets/icon.ico"
|
|
89
|
+
},
|
|
90
|
+
"update": {
|
|
91
|
+
"s3": {
|
|
92
|
+
"bucket": "end-of-life-dataset-cli-releases",
|
|
93
|
+
"host": "https://end-of-life-dataset-cli-releases.s3.amazonaws.com",
|
|
94
|
+
"acl": "bucket-owner-full-control"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
81
97
|
},
|
|
82
98
|
"types": "dist/index.d.ts"
|
|
83
99
|
}
|