@herodevs/cli 1.2.0-beta.1 → 1.3.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.3.0-beta.1 linux-x64 node-v22.14.0
20
20
  $ hd --help [COMMAND]
21
21
  USAGE
22
22
  $ hd COMMAND
@@ -80,7 +80,7 @@ EXAMPLES
80
80
  $ hd report committers --csv
81
81
  ```
82
82
 
83
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.2.0-beta.1/src/commands/report/committers.ts)_
83
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.3.0-beta.1/src/commands/report/committers.ts)_
84
84
 
85
85
  ## `hd report purls`
86
86
 
@@ -114,7 +114,7 @@ EXAMPLES
114
114
  $ hd report purls --save --csv
115
115
  ```
116
116
 
117
- _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.2.0-beta.1/src/commands/report/purls.ts)_
117
+ _See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.3.0-beta.1/src/commands/report/purls.ts)_
118
118
 
119
119
  ## `hd scan eol`
120
120
 
@@ -122,13 +122,14 @@ Scan a given sbom for EOL data
122
122
 
123
123
  ```
124
124
  USAGE
125
- $ hd scan eol [--json] [-f <value>] [-d <value>] [-s] [-a] [-c]
125
+ $ hd scan eol [--json] [-f <value>] [-p <value>] [-d <value>] [-s] [-a] [-c]
126
126
 
127
127
  FLAGS
128
128
  -a, --all Show all components (default is EOL and LTS only)
129
129
  -c, --getCustomerSupport Get Never-Ending Support for End-of-Life components
130
130
  -d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
131
131
  -f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
132
+ -p, --purls=<value> The file path of a list of purls to scan for EOL
132
133
  -s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
133
134
 
134
135
  GLOBAL FLAGS
@@ -142,10 +143,12 @@ EXAMPLES
142
143
 
143
144
  $ hd scan eol --file=path/to/sbom.json
144
145
 
146
+ $ hd scan eol --purls=path/to/purls.json
147
+
145
148
  $ hd scan eol -a --dir=./my-project
146
149
  ```
147
150
 
148
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.2.0-beta.1/src/commands/scan/eol.ts)_
151
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.3.0-beta.1/src/commands/scan/eol.ts)_
149
152
 
150
153
  ## `hd scan sbom`
151
154
 
@@ -173,5 +176,5 @@ EXAMPLES
173
176
  $ hd scan sbom --file=path/to/sbom.json
174
177
  ```
175
178
 
176
- _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.2.0-beta.1/src/commands/scan/sbom.ts)_
179
+ _See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.3.0-beta.1/src/commands/scan/sbom.ts)_
177
180
  <!-- 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.3.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,7 +38,7 @@
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",
43
44
  "graphql": "^16.8.1"
@@ -57,7 +58,7 @@
57
58
  "typescript": "^5.8.0"
58
59
  },
59
60
  "engines": {
60
- "node": ">=18.0.0"
61
+ "node": ">=20.0.0"
61
62
  },
62
63
  "files": [
63
64
  "./bin",