@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 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.2.0-beta.1 linux-x64 node-v22.14.0
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.2.0-beta.1/src/commands/report/committers.ts)_
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.2.0-beta.1/src/commands/report/purls.ts)_
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.2.0-beta.1/src/commands/scan/eol.ts)_
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.2.0-beta.1/src/commands/scan/sbom.ts)_
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 { ScanResult } from '../../api/types/nes.types.ts';
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
- sbom: (purls: string[]) => Promise<ScanResult>;
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
- sbom: (purls: string[]) => Promise<ScanResult>;
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
- * Uses the purls from the sbom to run the scan.
19
- */
20
- export declare function submitScan(purls: string[]): Promise<ScanResult>;
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 { SbomScanner } from "../../service/nes/nes.svc.js";
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
- sbom: SbomScanner(this),
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
- * Uses the purls from the sbom to run the scan.
20
+ * Submit a scan for a list of purls after they've been batched by batchSubmitPurls
20
21
  */
21
- export async function submitScan(purls) {
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.sbom(purls);
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
- export type ScanInput = {
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: 'SBOM';
4
- } | {
5
- type: 'OTHER';
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: ScanResponseReport;
16
+ eol: InsightsEolScanResult;
11
17
  };
12
18
  };
13
19
  }
14
- export interface ScanResponseReport {
15
- components: ScanResultComponent[];
16
- diagnostics?: Record<string, unknown>;
17
- message: string;
20
+ /**
21
+ * Result of the EOL scan operation
22
+ */
23
+ export interface InsightsEolScanResult {
24
+ scanId?: string;
18
25
  success: boolean;
19
- warnings?: ScanWarning[];
26
+ message: string;
27
+ components: InsightsEolScanComponent[];
28
+ warnings: ScanWarning[];
20
29
  }
21
- export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "LTS"];
22
- export type ComponentStatus = (typeof VALID_STATUSES)[number];
23
- export declare const isValidComponentStatus: (status: string) => status is ComponentStatus;
24
- export declare const validateComponentStatuses: (statuses: string[]) => ComponentStatus[];
25
- export interface ScanResultComponent {
26
- info: {
27
- eolAt: Date | null;
28
- isEol: boolean;
29
- daysEol: number | null;
30
- isUnsafe: boolean;
31
- status: ComponentStatus;
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 ScanResultComponentsMap = Map<string, ScanResultComponent>;
43
- export interface ScanResult {
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
- };
@@ -75,9 +75,7 @@ export default class ReportPurls extends Command {
75
75
  }
76
76
  }
77
77
  // Return wrapped object with metadata
78
- return {
79
- purls,
80
- };
78
+ return { purls };
81
79
  }
82
80
  catch (error) {
83
81
  this.error(`Failed to generate PURLs: ${getErrorMessage(error)}`);
@@ -1,19 +1,22 @@
1
1
  import { Command } from '@oclif/core';
2
- import type { ScanResultComponent } from '../../api/types/nes.types.ts';
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: ScanResultComponent[];
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 { submitScan } from "../../api/nes/nes.client.js";
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 sbom = await ScanSbom.loadSbom(flags, this.config);
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 scanSbom(sbom) {
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 submitScan(purls);
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
- fs.writeFileSync('nes.eol.json', JSON.stringify({ components }, null, 2));
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 { CdxCreator, CdxGenOptions } from './eol.svc.ts';
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<Sbom | undefined>;
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 { createBom } = await getCdxGen();
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<Sbom>;
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';
@@ -23,4 +23,3 @@ export function validateIsCycloneDxSbom(sbom) {
23
23
  throw new Error('Invalid SBOM: missing or invalid components array');
24
24
  }
25
25
  }
26
- export { cdxgen } from "./cdx.svc.js";
@@ -1,6 +1,5 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- // @ts-expect-error
4
3
  import { createBom } from '@cyclonedx/cdxgen';
5
4
  import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
6
5
  process.on('uncaughtException', (err) => {
@@ -1,4 +1,5 @@
1
1
  import type { NesApolloClient } from '../../api/nes/nes.client.ts';
2
- import type { ScanResponseReport, ScanResult } from '../../api/types/nes.types.ts';
3
- export declare const buildScanResult: (scan: ScanResponseReport) => ScanResult;
4
- export declare const SbomScanner: (client: NesApolloClient) => (purls: string[]) => Promise<ScanResult>;
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 input = { components: purls, type: 'SBOM' };
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
- const result = buildScanResult(scan);
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
+ }
@@ -1,4 +1,5 @@
1
- import type { ComponentStatus, ScanResultComponentsMap } from '../api/types/nes.types.ts';
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.2.0-beta.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=* ./bin/dev.js",
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.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": ">=18.0.0"
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
  }