@herodevs/cli 1.1.0-beta.1 → 1.2.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.
@@ -1,8 +1,9 @@
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 { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
4
5
  import { extractPurls, getPurlOutput } from "../../service/purls.svc.js";
5
- import SbomScan from "../scan/sbom.js";
6
+ import ScanSbom from "../scan/sbom.js";
6
7
  export default class ReportPurls extends Command {
7
8
  static description = 'Generate a list of purls from a sbom';
8
9
  static enableJsonFlag = true;
@@ -36,36 +37,50 @@ export default class ReportPurls extends Command {
36
37
  async run() {
37
38
  const { flags } = await this.parse(ReportPurls);
38
39
  const { dir: _dirFlag, file: _fileFlag, save, csv } = flags;
39
- const sbomArgs = SbomScan.getSbomArgs(flags);
40
- const sbomCommand = new SbomScan(sbomArgs, this.config);
41
- const sbom = await sbomCommand.run();
42
- if (!sbom) {
43
- throw new Error('SBOM not generated');
44
- }
45
- const purls = await extractPurls(sbom);
46
- this.log('Extracted %d purls from SBOM', purls.length);
47
- ux.action.stop('Scan completed');
48
- // Print the purls
49
- for (const purl of purls) {
50
- this.log(purl);
51
- }
52
- // Save if requested
53
- if (save) {
54
- try {
55
- const outputFile = csv && !this.jsonEnabled() ? 'csv' : 'json';
56
- const outputPath = path.join(_dirFlag || process.cwd(), `nes.purls.${outputFile}`);
57
- const purlOutput = getPurlOutput(purls, outputFile);
58
- fs.writeFileSync(outputPath, purlOutput);
59
- this.log('Purls saved to %s', outputPath);
40
+ try {
41
+ const sbom = await ScanSbom.loadSbom(flags, this.config);
42
+ const purls = await extractPurls(sbom);
43
+ this.log('Extracted %d purls from SBOM', purls.length);
44
+ ux.action.stop('Scan completed');
45
+ // Print the purls
46
+ for (const purl of purls) {
47
+ this.log(purl);
60
48
  }
61
- catch (error) {
62
- const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : 'Unknown error';
63
- this.warn(`Failed to save purls: ${errorMessage}`);
49
+ // Save if requested
50
+ if (save) {
51
+ try {
52
+ const outputFile = csv && !this.jsonEnabled() ? 'csv' : 'json';
53
+ const outputPath = path.join(_dirFlag || process.cwd(), `nes.purls.${outputFile}`);
54
+ const purlOutput = getPurlOutput(purls, outputFile);
55
+ fs.writeFileSync(outputPath, purlOutput);
56
+ this.log('Purls saved to %s', outputPath);
57
+ }
58
+ catch (error) {
59
+ if (isErrnoException(error)) {
60
+ switch (error.code) {
61
+ case 'EACCES':
62
+ this.error('Permission denied: Cannot write to output file');
63
+ break;
64
+ case 'ENOSPC':
65
+ this.error('No space left on device');
66
+ break;
67
+ case 'EISDIR':
68
+ this.error('Cannot write to output file: Is a directory');
69
+ break;
70
+ default:
71
+ this.error(`Failed to save purls: ${getErrorMessage(error)}`);
72
+ }
73
+ }
74
+ this.error(`Failed to save purls: ${getErrorMessage(error)}`);
75
+ }
64
76
  }
77
+ // Return wrapped object with metadata
78
+ return {
79
+ purls,
80
+ };
81
+ }
82
+ catch (error) {
83
+ this.error(`Failed to generate PURLs: ${getErrorMessage(error)}`);
65
84
  }
66
- // Return wrapped object with metadata
67
- return {
68
- purls,
69
- };
70
85
  }
71
86
  }
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import type { ScanResult } from '../../api/types/nes.types.ts';
2
+ import type { ScanResultComponent } from '../../api/types/nes.types.ts';
3
3
  export default class ScanEol extends Command {
4
4
  static description: string;
5
5
  static enableJsonFlag: boolean;
@@ -8,9 +8,18 @@ export default class ScanEol extends Command {
8
8
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ getCustomerSupport: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
13
  };
12
- run(): Promise<ScanResult | {
13
- components: [];
14
+ run(): Promise<{
15
+ components: ScanResultComponent[];
14
16
  }>;
15
- private checkEolScanDisabled;
17
+ private scanSbom;
18
+ private getFilteredComponents;
19
+ private saveReport;
20
+ private displayResults;
21
+ private displayNoComponentsMessage;
22
+ private logLine;
23
+ private displayStatusSection;
24
+ private logLegend;
16
25
  }
@@ -1,13 +1,18 @@
1
+ import fs from 'node:fs';
1
2
  import { Command, Flags, ux } from '@oclif/core';
2
- import { prepareRows, scanForEol } from "../../service/eol/eol.svc.js";
3
- import { promptComponentDetails } from "../../ui/eol.ui.js";
4
- import SbomScan from "./sbom.js";
3
+ import { submitScan } from "../../api/nes/nes.client.js";
4
+ import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
5
+ import { extractPurls } from "../../service/purls.svc.js";
6
+ import { createStatusDisplay } from "../../ui/eol.ui.js";
7
+ import { INDICATORS, STATUS_COLORS } from "../../ui/shared.us.js";
8
+ import ScanSbom from "./sbom.js";
5
9
  export default class ScanEol extends Command {
6
10
  static description = 'Scan a given sbom for EOL data';
7
11
  static enableJsonFlag = true;
8
12
  static examples = [
9
13
  '<%= config.bin %> <%= command.id %> --dir=./my-project',
10
14
  '<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
15
+ '<%= config.bin %> <%= command.id %> -a --dir=./my-project',
11
16
  ];
12
17
  static flags = {
13
18
  file: Flags.string({
@@ -23,52 +28,119 @@ export default class ScanEol extends Command {
23
28
  default: false,
24
29
  description: 'Save the generated SBOM as nes.sbom.json in the scanned directory',
25
30
  }),
31
+ all: Flags.boolean({
32
+ char: 'a',
33
+ description: 'Show all components (default is EOL and LTS only)',
34
+ default: false,
35
+ }),
36
+ getCustomerSupport: Flags.boolean({
37
+ char: 'c',
38
+ description: 'Get Never-Ending Support for End-of-Life components',
39
+ default: false,
40
+ }),
26
41
  };
27
42
  async run() {
28
- this.checkEolScanDisabled();
29
43
  const { flags } = await this.parse(ScanEol);
30
- const { dir: _dirFlag, file: _fileFlag } = flags;
31
- // Load the SBOM: Only pass the file, dir, and save flags to SbomScan
32
- const sbomArgs = SbomScan.getSbomArgs(flags);
33
- const sbomCommand = new SbomScan(sbomArgs, this.config);
34
- const sbom = await sbomCommand.run();
35
- if (!sbom) {
36
- throw new Error('SBOM not generated');
44
+ if (flags.getCustomerSupport) {
45
+ this.log(ux.colorize('yellow', 'Never-Ending Support is on the way. Please stay tuned for this feature.'));
46
+ }
47
+ const sbom = await ScanSbom.loadSbom(flags, this.config);
48
+ const scan = await this.scanSbom(sbom);
49
+ ux.action.stop('\nScan completed');
50
+ const filteredComponents = this.getFilteredComponents(scan, flags.all);
51
+ if (flags.save) {
52
+ await this.saveReport(filteredComponents);
53
+ }
54
+ if (this.jsonEnabled()) {
55
+ return { components: filteredComponents };
56
+ }
57
+ await this.displayResults(scan, flags.all);
58
+ return { components: filteredComponents };
59
+ }
60
+ async scanSbom(sbom) {
61
+ let scan;
62
+ let purls;
63
+ try {
64
+ purls = await extractPurls(sbom);
65
+ }
66
+ catch (error) {
67
+ this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
68
+ }
69
+ try {
70
+ scan = await submitScan(purls);
71
+ }
72
+ catch (error) {
73
+ this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
74
+ }
75
+ if (scan.components.size === 0) {
76
+ this.warn('No components found in scan');
37
77
  }
38
- // Scan the SBOM for EOL information
39
- const { purls, scan } = await scanForEol(sbom);
40
- ux.action.stop('Scan completed');
41
- if (!scan?.components) {
42
- if (_fileFlag) {
43
- throw new Error(`Scan failed to generate for file path: ${_fileFlag}`);
78
+ return scan;
79
+ }
80
+ getFilteredComponents(scan, all) {
81
+ return Array.from(scan.components.entries())
82
+ .filter(([_, component]) => all || ['EOL', 'LTS'].includes(component.info.status))
83
+ .map(([_, component]) => component);
84
+ }
85
+ async saveReport(components) {
86
+ try {
87
+ fs.writeFileSync('nes.eol.json', JSON.stringify({ components }, null, 2));
88
+ this.log('Report saved to nes.eol.json');
89
+ }
90
+ catch (error) {
91
+ if (isErrnoException(error)) {
92
+ switch (error.code) {
93
+ case 'EACCES':
94
+ this.error('Permission denied. Unable to save report to nes.eol.json');
95
+ break;
96
+ case 'ENOSPC':
97
+ this.error('No space left on device. Unable to save report to nes.eol.json');
98
+ break;
99
+ default:
100
+ this.error(`Failed to save report: ${getErrorMessage(error)}`);
101
+ }
44
102
  }
45
- if (_dirFlag) {
46
- throw new Error(`Scan failed to generate for dir: ${_dirFlag}`);
103
+ else {
104
+ this.error(`Failed to save report: ${getErrorMessage(error)}`);
47
105
  }
48
- throw new Error('Scan failed to generate components.');
49
106
  }
50
- const lines = await prepareRows(purls, scan);
51
- if (lines?.length === 0) {
52
- this.log('No dependencies found');
53
- return { components: [] };
107
+ }
108
+ async displayResults(scan, all) {
109
+ const { UNKNOWN, OK, LTS, EOL } = createStatusDisplay(scan.components, all);
110
+ if (!UNKNOWN.length && !OK.length && !LTS.length && !EOL.length) {
111
+ this.displayNoComponentsMessage(all);
112
+ return;
54
113
  }
55
- const r = await promptComponentDetails(lines);
56
- this.log('What now %o', r);
57
- return scan;
114
+ this.log(ux.colorize('bold', 'Here are the results of the scan:'));
115
+ this.logLine();
116
+ // Display sections in order of increasing severity
117
+ for (const components of [UNKNOWN, OK, LTS, EOL]) {
118
+ this.displayStatusSection(components);
119
+ }
120
+ this.logLegend();
58
121
  }
59
- checkEolScanDisabled(override = true) {
60
- // Check if running in beta version or pre v1.0.0
61
- const version = this.config.version;
62
- const [major] = version.split('.').map(Number);
63
- if (version.includes('beta') || major < 1) {
64
- this.log(`VERSION=${version}`);
65
- throw new Error('The EOL scan feature is not available in beta releases. Please wait for the stable release.');
122
+ displayNoComponentsMessage(all) {
123
+ if (!all) {
124
+ this.log(ux.colorize('yellow', 'No End-of-Life or Long Term Support components found in scan.'));
125
+ this.log(ux.colorize('yellow', 'Use --all flag to view all components.'));
66
126
  }
67
- // Just in case the beta check fails
68
- if (override) {
69
- this.log(`VERSION=${version}`);
70
- this.log('EOL scan is disabled');
71
- return { components: [] };
127
+ else {
128
+ this.log(ux.colorize('yellow', 'No components found in scan.'));
72
129
  }
73
130
  }
131
+ logLine() {
132
+ this.log(ux.colorize('bold', '-'.repeat(50)));
133
+ }
134
+ displayStatusSection(components) {
135
+ if (components.length > 0) {
136
+ this.log(components.join('\n'));
137
+ this.logLine();
138
+ }
139
+ }
140
+ logLegend() {
141
+ this.log(ux.colorize(`${STATUS_COLORS.UNKNOWN}`, `${INDICATORS.UNKNOWN} = No Known Issues`));
142
+ this.log(ux.colorize(`${STATUS_COLORS.OK}`, `${INDICATORS.OK} = OK`));
143
+ this.log(ux.colorize(`${STATUS_COLORS.LTS}`, `${INDICATORS.LTS}= Long Term Support (LTS)`));
144
+ this.log(ux.colorize(`${STATUS_COLORS.EOL}`, `${INDICATORS.EOL} = End of Life (EOL)`));
145
+ }
74
146
  }
@@ -10,6 +10,7 @@ export default class ScanSbom extends Command {
10
10
  save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  };
13
+ static loadSbom(flags: Record<string, string>, config: Command['config']): Promise<Sbom>;
13
14
  static getSbomArgs(flags: Record<string, string>): string[];
14
15
  getScanOptions(): {};
15
16
  run(): Promise<Sbom | undefined>;
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { Command, Flags, ux } from '@oclif/core';
5
5
  import { createSbom, validateIsCycloneDxSbom } from "../../service/eol/eol.svc.js";
6
+ import { getErrorMessage } from "../../service/error.svc.js";
6
7
  export default class ScanSbom extends Command {
7
8
  static description = 'Scan a SBOM for purls';
8
9
  static enableJsonFlag = true;
@@ -30,6 +31,15 @@ export default class ScanSbom extends Command {
30
31
  description: 'Run the scan in the background',
31
32
  }),
32
33
  };
34
+ static async loadSbom(flags, config) {
35
+ const sbomArgs = ScanSbom.getSbomArgs(flags);
36
+ const sbomCommand = new ScanSbom(sbomArgs, config);
37
+ const sbom = await sbomCommand.run();
38
+ if (!sbom) {
39
+ throw new Error('SBOM not generated');
40
+ }
41
+ return sbom;
42
+ }
33
43
  static getSbomArgs(flags) {
34
44
  const { dir, file, save, json, background } = flags ?? {};
35
45
  const sbomArgs = [];
@@ -37,8 +47,7 @@ export default class ScanSbom extends Command {
37
47
  sbomArgs.push('--file', file);
38
48
  if (dir)
39
49
  sbomArgs.push('--dir', dir);
40
- if (save)
41
- sbomArgs.push('--save');
50
+ // if (save) sbomArgs.push('--save'); // only save if sbom command is used directly with -s flag
42
51
  if (json)
43
52
  sbomArgs.push('--json');
44
53
  if (background)
@@ -54,7 +63,7 @@ export default class ScanSbom extends Command {
54
63
  const { dir, save, file, background } = flags;
55
64
  // Validate that exactly one of --file or --dir is provided
56
65
  if (file && dir) {
57
- throw new Error('Cannot specify both --file and --dir flags. Please use one or the other.');
66
+ this.error('Cannot specify both --file and --dir flags. Please use one or the other.');
58
67
  }
59
68
  let sbom;
60
69
  const path = dir || process.cwd();
@@ -76,39 +85,53 @@ export default class ScanSbom extends Command {
76
85
  }
77
86
  async _getSbomFromScan(_dirFlag) {
78
87
  const dir = resolve(_dirFlag);
79
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
80
- throw new Error(`Directory not found or not a directory: ${dir}`);
88
+ try {
89
+ if (!fs.existsSync(dir)) {
90
+ this.error(`Directory not found: ${dir}`);
91
+ }
92
+ const stats = fs.statSync(dir);
93
+ if (!stats.isDirectory()) {
94
+ this.error(`Path is not a directory: ${dir}`);
95
+ }
96
+ ux.action.start(`Scanning ${dir}`);
97
+ const options = this.getScanOptions();
98
+ const sbom = await createSbom(dir, options);
99
+ if (!sbom) {
100
+ this.error(`SBOM failed to generate for dir: ${dir}`);
101
+ }
102
+ return sbom;
81
103
  }
82
- ux.action.start(`Scanning ${dir}`);
83
- const options = this.getScanOptions();
84
- const sbom = await createSbom(dir, options);
85
- if (!sbom) {
86
- throw new Error(`SBOM failed to generate for dir: ${dir}`);
104
+ catch (error) {
105
+ this.error(`Failed to scan directory: ${getErrorMessage(error)}`);
87
106
  }
88
- return sbom;
89
107
  }
90
108
  _getSbomInBackground(path) {
91
- const opts = this.getScanOptions();
92
- const args = [
93
- JSON.stringify({
94
- opts,
95
- path,
96
- }),
97
- ];
98
- const workerProcess = spawn('node', [join(import.meta.dirname, '../../service/eol/sbom.worker.js'), ...args], {
99
- stdio: 'ignore',
100
- detached: true,
101
- env: { ...process.env },
102
- });
103
- workerProcess.unref();
109
+ try {
110
+ const opts = this.getScanOptions();
111
+ const args = [
112
+ JSON.stringify({
113
+ opts,
114
+ path,
115
+ }),
116
+ ];
117
+ const workerProcess = spawn('node', [join(import.meta.url, '../../service/eol/sbom.worker.js'), ...args], {
118
+ stdio: 'ignore',
119
+ detached: true,
120
+ env: { ...process.env },
121
+ });
122
+ workerProcess.unref();
123
+ }
124
+ catch (error) {
125
+ this.error(`Failed to start background scan: ${getErrorMessage(error)}`);
126
+ }
104
127
  }
105
128
  _getSbomFromFile(_fileFlag) {
106
129
  const file = resolve(_fileFlag);
107
- if (!fs.existsSync(file)) {
108
- throw new Error(`SBOM file not found: ${file}`);
109
- }
110
- ux.action.start(`Loading sbom from ${file}`);
111
130
  try {
131
+ if (!fs.existsSync(file)) {
132
+ this.error(`SBOM file not found: ${file}`);
133
+ }
134
+ ux.action.start(`Loading sbom from ${file}`);
112
135
  const fileContent = fs.readFileSync(file, {
113
136
  encoding: 'utf8',
114
137
  flag: 'r',
@@ -118,8 +141,7 @@ export default class ScanSbom extends Command {
118
141
  return sbom;
119
142
  }
120
143
  catch (error) {
121
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
122
- throw new Error(`Failed to read or parse SBOM file: ${errorMessage}`);
144
+ this.error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
123
145
  }
124
146
  }
125
147
  _saveSbom(dir, sbom) {
@@ -131,8 +153,7 @@ export default class ScanSbom extends Command {
131
153
  }
132
154
  }
133
155
  catch (error) {
134
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
135
- this.warn(`Failed to save SBOM: ${errorMessage}`);
156
+ this.error(`Failed to save SBOM: ${getErrorMessage(error)}`);
136
157
  }
137
158
  }
138
159
  }
@@ -21,7 +21,14 @@ export function parseGitLogOutput(output) {
21
21
  export function groupCommitsByMonth(entries) {
22
22
  const result = {};
23
23
  // Group commits by month
24
- const commitsByMonth = Object.groupBy(entries, (entry) => entry.month);
24
+ const commitsByMonth = entries.reduce((acc, entry) => {
25
+ const monthKey = entry.month;
26
+ if (!acc[monthKey]) {
27
+ acc[monthKey] = [];
28
+ }
29
+ acc[monthKey].push(entry);
30
+ return acc;
31
+ }, {});
25
32
  // Process each month
26
33
  for (const [month, commits] of Object.entries(commitsByMonth)) {
27
34
  if (!commits) {
@@ -29,7 +36,14 @@ export function groupCommitsByMonth(entries) {
29
36
  continue;
30
37
  }
31
38
  // Count commits per author for this month
32
- const commitsByAuthor = Object.groupBy(commits, (entry) => entry.author);
39
+ const commitsByAuthor = commits.reduce((acc, entry) => {
40
+ const authorKey = entry.author;
41
+ if (!acc[authorKey]) {
42
+ acc[authorKey] = [];
43
+ }
44
+ acc[authorKey].push(entry);
45
+ return acc;
46
+ }, {});
33
47
  const authorCounts = {};
34
48
  for (const [author, authorCommits] of Object.entries(commitsByAuthor)) {
35
49
  authorCounts[author] = authorCommits?.length ?? 0;
@@ -44,7 +58,14 @@ export function groupCommitsByMonth(entries) {
44
58
  * @returns Object with authors as keys and total commit counts as values
45
59
  */
46
60
  export function calculateOverallStats(entries) {
47
- const commitsByAuthor = Object.groupBy(entries, (entry) => entry.author);
61
+ const commitsByAuthor = entries.reduce((acc, entry) => {
62
+ const authorKey = entry.author;
63
+ if (!acc[authorKey]) {
64
+ acc[authorKey] = [];
65
+ }
66
+ acc[authorKey].push(entry);
67
+ return acc;
68
+ }, {});
48
69
  const result = {};
49
70
  // Count commits for each author
50
71
  for (const author in commitsByAuthor) {
@@ -72,6 +72,7 @@ export async function getCdxGen() {
72
72
  const ogEnv = process.env.NODE_ENV;
73
73
  process.env.NODE_ENV = undefined;
74
74
  try {
75
+ // @ts-expect-error
75
76
  cdxgen.createBom = (await import('@cyclonedx/cdxgen')).createBom;
76
77
  }
77
78
  finally {
@@ -1,5 +1,3 @@
1
- import type { ScanResult } from '../../api/types/nes.types.ts';
2
- import type { Line } from '../line.svc.ts';
3
1
  import { type Sbom } from './cdx.svc.ts';
4
2
  export interface CdxGenOptions {
5
3
  projectType?: string[];
@@ -12,23 +10,4 @@ export type CdxCreator = (dir: string, opts: CdxGenOptions) => Promise<{
12
10
  }>;
13
11
  export declare function createSbom(directory: string, opts?: ScanOptions): Promise<Sbom>;
14
12
  export declare function validateIsCycloneDxSbom(sbom: unknown): asserts sbom is Sbom;
15
- /**
16
- * Main function to scan directory and collect SBOM data
17
- */
18
- export declare function scanForEol(sbom: Sbom): Promise<{
19
- purls: string[];
20
- scan: ScanResult;
21
- }>;
22
- /**
23
- * Uses the purls from the sbom to run the scan.
24
- */
25
- export declare function submitScan(purls: string[]): Promise<ScanResult>;
26
- /**
27
- * Work in progress; creates "rows" for each component
28
- * based on the model + the scan result from NES.
29
- *
30
- * The idea being that each row can easily be used for
31
- * processing and/or rendering.
32
- */
33
- export declare function prepareRows(purls: string[], scan: ScanResult): Promise<Line[]>;
34
13
  export { cdxgen } from './cdx.svc.ts';
@@ -1,7 +1,4 @@
1
- import { NesApolloClient } from "../../api/nes/nes.client.js";
2
1
  import { debugLogger } from "../../service/log.svc.js";
3
- import { getDaysEolFromEolAt, getStatusFromComponent } from "../line.svc.js";
4
- import { extractPurls } from "../purls.svc.js";
5
2
  import { createBomFromDir } from "./cdx.svc.js";
6
3
  export async function createSbom(directory, opts = {}) {
7
4
  const sbom = await createBomFromDir(directory, opts.cdxgen || {});
@@ -26,61 +23,4 @@ export function validateIsCycloneDxSbom(sbom) {
26
23
  throw new Error('Invalid SBOM: missing or invalid components array');
27
24
  }
28
25
  }
29
- /**
30
- * Main function to scan directory and collect SBOM data
31
- */
32
- export async function scanForEol(sbom) {
33
- const purls = await extractPurls(sbom);
34
- const scan = await submitScan(purls);
35
- return { purls, scan };
36
- }
37
- /**
38
- * Uses the purls from the sbom to run the scan.
39
- */
40
- export async function submitScan(purls) {
41
- // NOTE: GRAPHQL_HOST is set in `./bin/dev.js` or tests
42
- const host = process.env.GRAPHQL_HOST || 'https://api.nes.herodevs.com';
43
- const path = process.env.GRAPHQL_PATH || '/graphql';
44
- const url = host + path;
45
- const client = new NesApolloClient(url);
46
- const scan = await client.scan.sbom(purls);
47
- return scan;
48
- }
49
- /**
50
- * Work in progress; creates "rows" for each component
51
- * based on the model + the scan result from NES.
52
- *
53
- * The idea being that each row can easily be used for
54
- * processing and/or rendering.
55
- */
56
- export async function prepareRows(purls, scan) {
57
- const lines = [];
58
- for (const purl of purls) {
59
- const details = scan.components.get(purl);
60
- if (!details) {
61
- // In this case, the purl string is in the generated sbom, but the NES/XEOL api has no data
62
- // TODO: add UNKNOWN Component Status, create new line, and create flag to show/hide unknown results
63
- debugLogger(`Unknown status: ${purl}.`);
64
- continue;
65
- }
66
- const { info } = details;
67
- // Handle date deserialization from GraphQL
68
- if (typeof info.eolAt === 'string' && info.eolAt) {
69
- info.eolAt = new Date(info.eolAt);
70
- }
71
- const daysEol = getDaysEolFromEolAt(info.eolAt);
72
- const status = getStatusFromComponent(details, daysEol);
73
- const showOk = process.env.SHOW_OK === 'true';
74
- if (!showOk && status === 'OK') {
75
- continue;
76
- }
77
- lines.push({
78
- daysEol,
79
- info,
80
- purl,
81
- status,
82
- });
83
- }
84
- return lines;
85
- }
86
26
  export { cdxgen } from "./cdx.svc.js";
@@ -1,5 +1,6 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ // @ts-expect-error
3
4
  import { createBom } from '@cyclonedx/cdxgen';
4
5
  import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
5
6
  process.on('uncaughtException', (err) => {
@@ -0,0 +1,8 @@
1
+ export declare const isError: (error: unknown) => error is Error;
2
+ export declare const isErrnoException: (error: unknown) => error is NodeJS.ErrnoException;
3
+ export declare const isApolloError: (error: unknown) => error is ApolloError;
4
+ export declare const getErrorMessage: (error: unknown) => string;
5
+ export declare class ApolloError extends Error {
6
+ readonly originalError?: unknown;
7
+ constructor(message: string, original?: unknown);
8
+ }
@@ -0,0 +1,28 @@
1
+ export const isError = (error) => {
2
+ return error instanceof Error;
3
+ };
4
+ export const isErrnoException = (error) => {
5
+ return isError(error) && 'code' in error;
6
+ };
7
+ export const isApolloError = (error) => {
8
+ return error instanceof ApolloError;
9
+ };
10
+ export const getErrorMessage = (error) => {
11
+ if (isError(error)) {
12
+ return error.message;
13
+ }
14
+ return 'Unknown error';
15
+ };
16
+ export class ApolloError extends Error {
17
+ originalError;
18
+ constructor(message, original) {
19
+ if (isError(original)) {
20
+ super(`${message}: ${original.message}`);
21
+ }
22
+ else {
23
+ super(`${message}: ${String(original)}`);
24
+ }
25
+ this.name = 'ApolloError';
26
+ this.originalError = original;
27
+ }
28
+ }
@@ -9,6 +9,7 @@ export const buildScanResult = (scan) => {
9
9
  components,
10
10
  message: scan.message,
11
11
  success: true,
12
+ warnings: scan.warnings || [],
12
13
  };
13
14
  };
14
15
  export const SbomScanner = (client) => async (purls) => {
@@ -0,0 +1 @@
1
+ export declare function parseMomentToSimpleDate(momentDate: string | Date | number | null): string;