@herodevs/cli 1.0.0-beta.2 → 1.1.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 +22 -21
- package/bin/dev.js +0 -1
- package/dist/commands/report/committers.d.ts +3 -2
- package/dist/commands/report/committers.js +40 -15
- package/dist/commands/report/purls.d.ts +4 -2
- package/dist/commands/report/purls.js +19 -14
- package/dist/commands/scan/eol.js +3 -0
- package/dist/commands/scan/sbom.d.ts +3 -1
- package/dist/commands/scan/sbom.js +46 -14
- package/dist/hooks/prerun.js +8 -0
- package/dist/service/eol/cdx.svc.d.ts +52 -0
- package/dist/service/eol/cdx.svc.js +57 -62
- package/dist/service/eol/eol.svc.js +3 -3
- package/dist/service/eol/sbom.worker.d.ts +1 -0
- package/dist/service/eol/sbom.worker.js +25 -0
- package/dist/service/log.svc.d.ts +5 -8
- package/dist/service/log.svc.js +5 -18
- package/dist/service/nes/nes.svc.js +3 -3
- package/dist/service/purls.svc.js +1 -1
- package/package.json +8 -9
- package/dist/hooks/init/update.d.ts +0 -2
- package/dist/hooks/init/update.js +0 -5
- package/dist/hooks/prerun/CommandContextHook.js +0 -8
- /package/dist/hooks/{prerun/CommandContextHook.d.ts → prerun.d.ts} +0 -0
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.1.0-beta.1 linux-x64 node-v22.14.0
|
|
20
20
|
$ hd --help [COMMAND]
|
|
21
21
|
USAGE
|
|
22
22
|
$ hd COMMAND
|
|
@@ -357,13 +357,12 @@ Generate report of committers to a git repository
|
|
|
357
357
|
|
|
358
358
|
```
|
|
359
359
|
USAGE
|
|
360
|
-
$ hd report committers [--json] [-m <value>] [-
|
|
360
|
+
$ hd report committers [--json] [-m <value>] [-c] [-s]
|
|
361
361
|
|
|
362
362
|
FLAGS
|
|
363
|
-
-
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
-s, --save Save the committers report as nes.committers.<output>
|
|
363
|
+
-c, --csv Output in CSV format
|
|
364
|
+
-m, --months=<value> [default: 12] The number of months of git history to review
|
|
365
|
+
-s, --save Save the committers report as nes.committers.<output>
|
|
367
366
|
|
|
368
367
|
GLOBAL FLAGS
|
|
369
368
|
--json Format output as json.
|
|
@@ -374,14 +373,14 @@ DESCRIPTION
|
|
|
374
373
|
EXAMPLES
|
|
375
374
|
$ hd report committers
|
|
376
375
|
|
|
377
|
-
$ hd report committers
|
|
376
|
+
$ hd report committers --csv -s
|
|
378
377
|
|
|
379
|
-
$ hd report committers --
|
|
378
|
+
$ hd report committers --json
|
|
380
379
|
|
|
381
|
-
$ hd report committers --
|
|
380
|
+
$ hd report committers --csv
|
|
382
381
|
```
|
|
383
382
|
|
|
384
|
-
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.
|
|
383
|
+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/report/committers.ts)_
|
|
385
384
|
|
|
386
385
|
## `hd report purls`
|
|
387
386
|
|
|
@@ -389,14 +388,13 @@ Generate a list of purls from a sbom
|
|
|
389
388
|
|
|
390
389
|
```
|
|
391
390
|
USAGE
|
|
392
|
-
$ hd report purls [--json] [-f <value>] [-d <value>] [-s] [-
|
|
391
|
+
$ hd report purls [--json] [-f <value>] [-d <value>] [-s] [-c]
|
|
393
392
|
|
|
394
393
|
FLAGS
|
|
395
|
-
-
|
|
396
|
-
-
|
|
397
|
-
-
|
|
398
|
-
|
|
399
|
-
-s, --save Save the list of purls as nes.purls.<output>
|
|
394
|
+
-c, --csv Save output in CSV format (only applies when using --save)
|
|
395
|
+
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
396
|
+
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
397
|
+
-s, --save Save the list of purls as nes.purls.<output>
|
|
400
398
|
|
|
401
399
|
GLOBAL FLAGS
|
|
402
400
|
--json Format output as json.
|
|
@@ -405,16 +403,18 @@ DESCRIPTION
|
|
|
405
403
|
Generate a list of purls from a sbom
|
|
406
404
|
|
|
407
405
|
EXAMPLES
|
|
406
|
+
$ hd report purls --json -s
|
|
407
|
+
|
|
408
408
|
$ hd report purls --dir=./my-project
|
|
409
409
|
|
|
410
410
|
$ hd report purls --file=path/to/sbom.json
|
|
411
411
|
|
|
412
412
|
$ hd report purls --dir=./my-project --save
|
|
413
413
|
|
|
414
|
-
$ hd report purls --save --
|
|
414
|
+
$ hd report purls --save --csv
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
-
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.
|
|
417
|
+
_See code: [src/commands/report/purls.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/report/purls.ts)_
|
|
418
418
|
|
|
419
419
|
## `hd scan eol`
|
|
420
420
|
|
|
@@ -441,7 +441,7 @@ EXAMPLES
|
|
|
441
441
|
$ hd scan eol --file=path/to/sbom.json
|
|
442
442
|
```
|
|
443
443
|
|
|
444
|
-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.
|
|
444
|
+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/scan/eol.ts)_
|
|
445
445
|
|
|
446
446
|
## `hd scan sbom`
|
|
447
447
|
|
|
@@ -449,9 +449,10 @@ Scan a SBOM for purls
|
|
|
449
449
|
|
|
450
450
|
```
|
|
451
451
|
USAGE
|
|
452
|
-
$ hd scan sbom [--json] [-f <value>] [-d <value>] [-s]
|
|
452
|
+
$ hd scan sbom [--json] [-f <value>] [-d <value>] [-s] [-b]
|
|
453
453
|
|
|
454
454
|
FLAGS
|
|
455
|
+
-b, --background Run the scan in the background
|
|
455
456
|
-d, --dir=<value> The directory to scan in order to create a cyclonedx sbom
|
|
456
457
|
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
|
|
457
458
|
-s, --save Save the generated SBOM as nes.sbom.json in the scanned directory
|
|
@@ -468,5 +469,5 @@ EXAMPLES
|
|
|
468
469
|
$ hd scan sbom --file=path/to/sbom.json
|
|
469
470
|
```
|
|
470
471
|
|
|
471
|
-
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.
|
|
472
|
+
_See code: [src/commands/scan/sbom.ts](https://github.com/herodevs/cli/blob/v1.1.0-beta.1/src/commands/scan/sbom.ts)_
|
|
472
473
|
<!-- commandsstop -->
|
package/bin/dev.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { type ReportData } from '../../service/committers.svc.ts';
|
|
2
3
|
export default class Committers extends Command {
|
|
3
4
|
static description: string;
|
|
4
5
|
static enableJsonFlag: boolean;
|
|
5
6
|
static examples: string[];
|
|
6
7
|
static flags: {
|
|
7
8
|
months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
|
|
9
|
+
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
|
-
run(): Promise<
|
|
12
|
+
run(): Promise<ReportData | string>;
|
|
12
13
|
/**
|
|
13
14
|
* Generates structured report data
|
|
14
15
|
* @param entries - parsed git log output for commits
|
|
@@ -2,15 +2,15 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { calculateOverallStats,
|
|
5
|
+
import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
|
|
6
6
|
export default class Committers extends Command {
|
|
7
7
|
static description = 'Generate report of committers to a git repository';
|
|
8
8
|
static enableJsonFlag = true;
|
|
9
9
|
static examples = [
|
|
10
10
|
'<%= config.bin %> <%= command.id %>',
|
|
11
|
-
'<%= config.bin %> <%= command.id %>
|
|
12
|
-
'<%= config.bin %> <%= command.id %> --
|
|
13
|
-
'<%= config.bin %> <%= command.id %> --
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --csv -s',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --csv',
|
|
14
14
|
];
|
|
15
15
|
static flags = {
|
|
16
16
|
months: Flags.integer({
|
|
@@ -18,11 +18,10 @@ export default class Committers extends Command {
|
|
|
18
18
|
description: 'The number of months of git history to review',
|
|
19
19
|
default: 12,
|
|
20
20
|
}),
|
|
21
|
-
|
|
22
|
-
char: '
|
|
23
|
-
description: 'Output
|
|
24
|
-
|
|
25
|
-
default: 'text',
|
|
21
|
+
csv: Flags.boolean({
|
|
22
|
+
char: 'c',
|
|
23
|
+
description: 'Output in CSV format',
|
|
24
|
+
default: false,
|
|
26
25
|
}),
|
|
27
26
|
save: Flags.boolean({
|
|
28
27
|
char: 's',
|
|
@@ -32,24 +31,50 @@ export default class Committers extends Command {
|
|
|
32
31
|
};
|
|
33
32
|
async run() {
|
|
34
33
|
const { flags } = await this.parse(Committers);
|
|
35
|
-
const { months,
|
|
34
|
+
const { months, csv, save } = flags;
|
|
35
|
+
const isJson = this.jsonEnabled();
|
|
36
36
|
const sinceDate = `${months} months ago`;
|
|
37
|
+
this.log('Starting committers report with flags: %O', flags);
|
|
37
38
|
try {
|
|
38
39
|
// Generate structured report data
|
|
39
40
|
const entries = this.fetchGitCommitData(sinceDate);
|
|
41
|
+
this.log('Fetched %d commit entries', entries.length);
|
|
40
42
|
const reportData = this.generateReportData(entries);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// Handle different output scenarios
|
|
44
|
+
if (isJson) {
|
|
45
|
+
// JSON mode
|
|
46
|
+
if (save) {
|
|
47
|
+
fs.writeFileSync(path.resolve('nes.committers.json'), JSON.stringify(reportData, null, 2));
|
|
48
|
+
this.log('Report written to json');
|
|
49
|
+
}
|
|
50
|
+
return reportData;
|
|
51
|
+
}
|
|
52
|
+
if (csv) {
|
|
53
|
+
// CSV mode
|
|
54
|
+
const csvOutput = formatAsCsv(reportData);
|
|
55
|
+
if (save) {
|
|
56
|
+
fs.writeFileSync(path.resolve('nes.committers.csv'), csvOutput);
|
|
57
|
+
this.log('Report written to csv');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.log(csvOutput);
|
|
61
|
+
}
|
|
62
|
+
return csvOutput;
|
|
63
|
+
}
|
|
64
|
+
// Text mode
|
|
65
|
+
const textOutput = formatAsText(reportData);
|
|
43
66
|
if (save) {
|
|
44
|
-
fs.writeFileSync(path.resolve(
|
|
45
|
-
this.log(
|
|
67
|
+
fs.writeFileSync(path.resolve('nes.committers.txt'), textOutput);
|
|
68
|
+
this.log('Report written to txt');
|
|
46
69
|
}
|
|
47
70
|
else {
|
|
48
|
-
this.log(
|
|
71
|
+
this.log(textOutput);
|
|
49
72
|
}
|
|
73
|
+
return textOutput;
|
|
50
74
|
}
|
|
51
75
|
catch (error) {
|
|
52
76
|
this.error(`Failed to generate report: ${error.message}`);
|
|
77
|
+
throw error;
|
|
53
78
|
}
|
|
54
79
|
}
|
|
55
80
|
/**
|
|
@@ -7,7 +7,9 @@ export default class ReportPurls extends Command {
|
|
|
7
7
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
|
|
10
|
+
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
};
|
|
12
|
-
run(): Promise<
|
|
12
|
+
run(): Promise<{
|
|
13
|
+
purls: string[];
|
|
14
|
+
}>;
|
|
13
15
|
}
|
|
@@ -7,10 +7,11 @@ export default class ReportPurls extends Command {
|
|
|
7
7
|
static description = 'Generate a list of purls from a sbom';
|
|
8
8
|
static enableJsonFlag = true;
|
|
9
9
|
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --json -s',
|
|
10
11
|
'<%= config.bin %> <%= command.id %> --dir=./my-project',
|
|
11
12
|
'<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
|
|
12
13
|
'<%= config.bin %> <%= command.id %> --dir=./my-project --save',
|
|
13
|
-
'<%= config.bin %> <%= command.id %> --save --
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --save --csv',
|
|
14
15
|
];
|
|
15
16
|
static flags = {
|
|
16
17
|
file: Flags.string({
|
|
@@ -26,41 +27,45 @@ export default class ReportPurls extends Command {
|
|
|
26
27
|
default: false,
|
|
27
28
|
description: 'Save the list of purls as nes.purls.<output>',
|
|
28
29
|
}),
|
|
29
|
-
|
|
30
|
-
char: '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
description: 'The format of the saved file (when using --save)',
|
|
30
|
+
csv: Flags.boolean({
|
|
31
|
+
char: 'c',
|
|
32
|
+
default: false,
|
|
33
|
+
description: 'Save output in CSV format (only applies when using --save)',
|
|
34
34
|
}),
|
|
35
35
|
};
|
|
36
36
|
async run() {
|
|
37
37
|
const { flags } = await this.parse(ReportPurls);
|
|
38
|
-
const { dir: _dirFlag, file: _fileFlag, save,
|
|
39
|
-
// Load the SBOM: Only pass the file, dir, and save flags to SbomScan
|
|
38
|
+
const { dir: _dirFlag, file: _fileFlag, save, csv } = flags;
|
|
40
39
|
const sbomArgs = SbomScan.getSbomArgs(flags);
|
|
41
40
|
const sbomCommand = new SbomScan(sbomArgs, this.config);
|
|
42
41
|
const sbom = await sbomCommand.run();
|
|
43
|
-
|
|
42
|
+
if (!sbom) {
|
|
43
|
+
throw new Error('SBOM not generated');
|
|
44
|
+
}
|
|
44
45
|
const purls = await extractPurls(sbom);
|
|
46
|
+
this.log('Extracted %d purls from SBOM', purls.length);
|
|
45
47
|
ux.action.stop('Scan completed');
|
|
46
48
|
// Print the purls
|
|
47
|
-
this.log('Found purls:');
|
|
48
49
|
for (const purl of purls) {
|
|
49
50
|
this.log(purl);
|
|
50
51
|
}
|
|
51
52
|
// Save if requested
|
|
52
53
|
if (save) {
|
|
53
54
|
try {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
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);
|
|
56
58
|
fs.writeFileSync(outputPath, purlOutput);
|
|
57
|
-
this.log(
|
|
59
|
+
this.log('Purls saved to %s', outputPath);
|
|
58
60
|
}
|
|
59
61
|
catch (error) {
|
|
60
62
|
const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : 'Unknown error';
|
|
61
63
|
this.warn(`Failed to save purls: ${errorMessage}`);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
// Return wrapped object with metadata
|
|
67
|
+
return {
|
|
68
|
+
purls,
|
|
69
|
+
};
|
|
65
70
|
}
|
|
66
71
|
}
|
|
@@ -32,6 +32,9 @@ export default class ScanEol extends Command {
|
|
|
32
32
|
const sbomArgs = SbomScan.getSbomArgs(flags);
|
|
33
33
|
const sbomCommand = new SbomScan(sbomArgs, this.config);
|
|
34
34
|
const sbom = await sbomCommand.run();
|
|
35
|
+
if (!sbom) {
|
|
36
|
+
throw new Error('SBOM not generated');
|
|
37
|
+
}
|
|
35
38
|
// Scan the SBOM for EOL information
|
|
36
39
|
const { purls, scan } = await scanForEol(sbom);
|
|
37
40
|
ux.action.stop('Scan completed');
|
|
@@ -8,11 +8,13 @@ export default class ScanSbom 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
|
+
background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
static getSbomArgs(flags: Record<string, string>): string[];
|
|
13
14
|
getScanOptions(): {};
|
|
14
|
-
run(): Promise<Sbom>;
|
|
15
|
+
run(): Promise<Sbom | undefined>;
|
|
15
16
|
private _getSbomFromScan;
|
|
17
|
+
private _getSbomInBackground;
|
|
16
18
|
private _getSbomFromFile;
|
|
17
19
|
private _saveSbom;
|
|
18
20
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Command, Flags, ux } from '@oclif/core';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
3
2
|
import fs from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { Command, Flags, ux } from '@oclif/core';
|
|
4
5
|
import { createSbom, validateIsCycloneDxSbom } from "../../service/eol/eol.svc.js";
|
|
5
6
|
export default class ScanSbom extends Command {
|
|
6
7
|
static description = 'Scan a SBOM for purls';
|
|
@@ -23,9 +24,14 @@ export default class ScanSbom extends Command {
|
|
|
23
24
|
default: false,
|
|
24
25
|
description: 'Save the generated SBOM as nes.sbom.json in the scanned directory',
|
|
25
26
|
}),
|
|
27
|
+
background: Flags.boolean({
|
|
28
|
+
char: 'b',
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'Run the scan in the background',
|
|
31
|
+
}),
|
|
26
32
|
};
|
|
27
33
|
static getSbomArgs(flags) {
|
|
28
|
-
const { dir, file, save } = flags ?? {};
|
|
34
|
+
const { dir, file, save, json, background } = flags ?? {};
|
|
29
35
|
const sbomArgs = [];
|
|
30
36
|
if (file)
|
|
31
37
|
sbomArgs.push('--file', file);
|
|
@@ -33,6 +39,10 @@ export default class ScanSbom extends Command {
|
|
|
33
39
|
sbomArgs.push('--dir', dir);
|
|
34
40
|
if (save)
|
|
35
41
|
sbomArgs.push('--save');
|
|
42
|
+
if (json)
|
|
43
|
+
sbomArgs.push('--json');
|
|
44
|
+
if (background)
|
|
45
|
+
sbomArgs.push('--background');
|
|
36
46
|
return sbomArgs;
|
|
37
47
|
}
|
|
38
48
|
getScanOptions() {
|
|
@@ -41,26 +51,31 @@ export default class ScanSbom extends Command {
|
|
|
41
51
|
}
|
|
42
52
|
async run() {
|
|
43
53
|
const { flags } = await this.parse(ScanSbom);
|
|
44
|
-
const { dir
|
|
54
|
+
const { dir, save, file, background } = flags;
|
|
45
55
|
// Validate that exactly one of --file or --dir is provided
|
|
46
|
-
if (
|
|
56
|
+
if (file && dir) {
|
|
47
57
|
throw new Error('Cannot specify both --file and --dir flags. Please use one or the other.');
|
|
48
58
|
}
|
|
49
59
|
let sbom;
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const path = dir || process.cwd();
|
|
61
|
+
if (file) {
|
|
62
|
+
sbom = this._getSbomFromFile(file);
|
|
63
|
+
}
|
|
64
|
+
else if (background) {
|
|
65
|
+
this._getSbomInBackground(path);
|
|
66
|
+
this.log(`The scan is running in the background. The file will be saved at ${path}/nes.sbom.json`);
|
|
67
|
+
return;
|
|
52
68
|
}
|
|
53
69
|
else {
|
|
54
|
-
|
|
55
|
-
sbom = await this._getSbomFromScan(_dir);
|
|
70
|
+
sbom = await this._getSbomFromScan(path);
|
|
56
71
|
if (save) {
|
|
57
|
-
this._saveSbom(
|
|
72
|
+
this._saveSbom(path, sbom);
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
return sbom;
|
|
61
76
|
}
|
|
62
77
|
async _getSbomFromScan(_dirFlag) {
|
|
63
|
-
const dir =
|
|
78
|
+
const dir = resolve(_dirFlag);
|
|
64
79
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
65
80
|
throw new Error(`Directory not found or not a directory: ${dir}`);
|
|
66
81
|
}
|
|
@@ -72,8 +87,23 @@ export default class ScanSbom extends Command {
|
|
|
72
87
|
}
|
|
73
88
|
return sbom;
|
|
74
89
|
}
|
|
90
|
+
_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();
|
|
104
|
+
}
|
|
75
105
|
_getSbomFromFile(_fileFlag) {
|
|
76
|
-
const file =
|
|
106
|
+
const file = resolve(_fileFlag);
|
|
77
107
|
if (!fs.existsSync(file)) {
|
|
78
108
|
throw new Error(`SBOM file not found: ${file}`);
|
|
79
109
|
}
|
|
@@ -94,9 +124,11 @@ export default class ScanSbom extends Command {
|
|
|
94
124
|
}
|
|
95
125
|
_saveSbom(dir, sbom) {
|
|
96
126
|
try {
|
|
97
|
-
const outputPath =
|
|
127
|
+
const outputPath = join(dir, 'nes.sbom.json');
|
|
98
128
|
fs.writeFileSync(outputPath, JSON.stringify(sbom, null, 2));
|
|
99
|
-
this.
|
|
129
|
+
if (!this.jsonEnabled()) {
|
|
130
|
+
this.log(`SBOM saved to ${outputPath}`);
|
|
131
|
+
}
|
|
100
132
|
}
|
|
101
133
|
catch (error) {
|
|
102
134
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -9,6 +9,58 @@ export interface Sbom {
|
|
|
9
9
|
components: SbomEntry[];
|
|
10
10
|
dependencies: SbomEntry[];
|
|
11
11
|
}
|
|
12
|
+
export declare const SBOM_DEFAULT__OPTIONS: {
|
|
13
|
+
$0: string;
|
|
14
|
+
_: never[];
|
|
15
|
+
'auto-compositions': boolean;
|
|
16
|
+
autoCompositions: boolean;
|
|
17
|
+
'data-flow-slices-file': string;
|
|
18
|
+
dataFlowSlicesFile: string;
|
|
19
|
+
deep: boolean;
|
|
20
|
+
'deps-slices-file': string;
|
|
21
|
+
depsSlicesFile: string;
|
|
22
|
+
evidence: boolean;
|
|
23
|
+
'export-proto': boolean;
|
|
24
|
+
exportProto: boolean;
|
|
25
|
+
'fail-on-error': boolean;
|
|
26
|
+
failOnError: boolean;
|
|
27
|
+
false: boolean;
|
|
28
|
+
'include-crypto': boolean;
|
|
29
|
+
'include-formulation': boolean;
|
|
30
|
+
includeCrypto: boolean;
|
|
31
|
+
includeFormulation: boolean;
|
|
32
|
+
'install-deps': boolean;
|
|
33
|
+
installDeps: boolean;
|
|
34
|
+
'min-confidence': number;
|
|
35
|
+
minConfidence: number;
|
|
36
|
+
multiProject: boolean;
|
|
37
|
+
'no-banner': boolean;
|
|
38
|
+
noBabel: boolean;
|
|
39
|
+
noBanner: boolean;
|
|
40
|
+
o: string;
|
|
41
|
+
output: string;
|
|
42
|
+
outputFormat: string;
|
|
43
|
+
profile: string;
|
|
44
|
+
project: undefined;
|
|
45
|
+
'project-version': string;
|
|
46
|
+
projectVersion: string;
|
|
47
|
+
'proto-bin-file': string;
|
|
48
|
+
protoBinFile: string;
|
|
49
|
+
r: boolean;
|
|
50
|
+
'reachables-slices-file': string;
|
|
51
|
+
reachablesSlicesFile: string;
|
|
52
|
+
recurse: boolean;
|
|
53
|
+
requiredOnly: boolean;
|
|
54
|
+
'semantics-slices-file': string;
|
|
55
|
+
semanticsSlicesFile: string;
|
|
56
|
+
'skip-dt-tls-check': boolean;
|
|
57
|
+
skipDtTlsCheck: boolean;
|
|
58
|
+
'spec-version': number;
|
|
59
|
+
specVersion: number;
|
|
60
|
+
'usages-slices-file': string;
|
|
61
|
+
usagesSlicesFile: string;
|
|
62
|
+
validate: boolean;
|
|
63
|
+
};
|
|
12
64
|
/**
|
|
13
65
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
14
66
|
* `directory`, and returns the `bomJson` property.
|
|
@@ -1,71 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { debugLogger } from "../../service/log.svc.js";
|
|
2
|
+
export const SBOM_DEFAULT__OPTIONS = {
|
|
3
|
+
$0: 'cdxgen',
|
|
4
|
+
_: [],
|
|
5
|
+
'auto-compositions': true,
|
|
6
|
+
autoCompositions: true,
|
|
7
|
+
'data-flow-slices-file': 'data-flow.slices.json',
|
|
8
|
+
dataFlowSlicesFile: 'data-flow.slices.json',
|
|
9
|
+
deep: false, // TODO: you def want to check this out
|
|
10
|
+
'deps-slices-file': 'deps.slices.json',
|
|
11
|
+
depsSlicesFile: 'deps.slices.json',
|
|
12
|
+
evidence: false,
|
|
13
|
+
'export-proto': false,
|
|
14
|
+
exportProto: false,
|
|
15
|
+
// DON'T FAIL ON ERROR; you won't get hlepful logs
|
|
16
|
+
'fail-on-error': false,
|
|
17
|
+
failOnError: false,
|
|
18
|
+
false: true,
|
|
19
|
+
'include-crypto': false,
|
|
20
|
+
'include-formulation': false,
|
|
21
|
+
includeCrypto: false,
|
|
22
|
+
includeFormulation: false,
|
|
23
|
+
'install-deps': true,
|
|
24
|
+
installDeps: true,
|
|
25
|
+
'min-confidence': 0,
|
|
26
|
+
minConfidence: 0,
|
|
27
|
+
multiProject: true,
|
|
28
|
+
'no-banner': false,
|
|
29
|
+
noBabel: false,
|
|
30
|
+
noBanner: false,
|
|
31
|
+
o: 'bom.json',
|
|
32
|
+
output: 'bom.json',
|
|
33
|
+
outputFormat: 'json', // or "xml"
|
|
34
|
+
// author: ['OWASP Foundation'],
|
|
35
|
+
profile: 'generic',
|
|
36
|
+
project: undefined,
|
|
37
|
+
'project-version': '',
|
|
38
|
+
projectVersion: '',
|
|
39
|
+
'proto-bin-file': 'bom.cdx',
|
|
40
|
+
protoBinFile: 'bom.cdx',
|
|
41
|
+
r: false,
|
|
42
|
+
'reachables-slices-file': 'reachables.slices.json',
|
|
43
|
+
reachablesSlicesFile: 'reachables.slices.json',
|
|
44
|
+
recurse: false,
|
|
45
|
+
requiredOnly: false,
|
|
46
|
+
'semantics-slices-file': 'semantics.slices.json',
|
|
47
|
+
semanticsSlicesFile: 'semantics.slices.json',
|
|
48
|
+
'skip-dt-tls-check': true,
|
|
49
|
+
skipDtTlsCheck: true,
|
|
50
|
+
'spec-version': 1.6,
|
|
51
|
+
specVersion: 1.6,
|
|
52
|
+
'usages-slices-file': 'usages.slices.json',
|
|
53
|
+
usagesSlicesFile: 'usages.slices.json',
|
|
54
|
+
validate: true,
|
|
55
|
+
};
|
|
2
56
|
/**
|
|
3
57
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
4
58
|
* `directory`, and returns the `bomJson` property.
|
|
5
59
|
*/
|
|
6
60
|
export async function createBomFromDir(directory, opts = {}) {
|
|
7
|
-
const options = {
|
|
8
|
-
$0: 'cdxgen',
|
|
9
|
-
_: [],
|
|
10
|
-
'auto-compositions': true,
|
|
11
|
-
autoCompositions: true,
|
|
12
|
-
'data-flow-slices-file': 'data-flow.slices.json',
|
|
13
|
-
dataFlowSlicesFile: 'data-flow.slices.json',
|
|
14
|
-
deep: false, // TODO: you def want to check this out
|
|
15
|
-
'deps-slices-file': 'deps.slices.json',
|
|
16
|
-
depsSlicesFile: 'deps.slices.json',
|
|
17
|
-
evidence: false,
|
|
18
|
-
'export-proto': false,
|
|
19
|
-
exportProto: false,
|
|
20
|
-
// DON'T FAIL ON ERROR; you won't get hlepful logs
|
|
21
|
-
'fail-on-error': false,
|
|
22
|
-
failOnError: false,
|
|
23
|
-
false: true,
|
|
24
|
-
'include-crypto': false,
|
|
25
|
-
'include-formulation': false,
|
|
26
|
-
includeCrypto: false,
|
|
27
|
-
includeFormulation: false,
|
|
28
|
-
// 'server-host': '127.0.0.1',
|
|
29
|
-
// serverHost: '127.0.0.1',
|
|
30
|
-
// 'server-port': '9090',
|
|
31
|
-
// serverPort: '9090',
|
|
32
|
-
'install-deps': true,
|
|
33
|
-
installDeps: true,
|
|
34
|
-
'min-confidence': 0,
|
|
35
|
-
minConfidence: 0,
|
|
36
|
-
multiProject: true,
|
|
37
|
-
'no-banner': false,
|
|
38
|
-
noBabel: false,
|
|
39
|
-
noBanner: false,
|
|
40
|
-
o: 'bom.json',
|
|
41
|
-
output: 'bom.json',
|
|
42
|
-
outputFormat: 'json', // or "xml"
|
|
43
|
-
// author: ['OWASP Foundation'],
|
|
44
|
-
profile: 'generic',
|
|
45
|
-
project: undefined,
|
|
46
|
-
'project-version': '',
|
|
47
|
-
projectVersion: '',
|
|
48
|
-
'proto-bin-file': 'bom.cdx',
|
|
49
|
-
protoBinFile: 'bom.cdx',
|
|
50
|
-
r: false,
|
|
51
|
-
'reachables-slices-file': 'reachables.slices.json',
|
|
52
|
-
reachablesSlicesFile: 'reachables.slices.json',
|
|
53
|
-
recurse: false,
|
|
54
|
-
requiredOnly: false,
|
|
55
|
-
'semantics-slices-file': 'semantics.slices.json',
|
|
56
|
-
semanticsSlicesFile: 'semantics.slices.json',
|
|
57
|
-
'skip-dt-tls-check': true,
|
|
58
|
-
skipDtTlsCheck: true,
|
|
59
|
-
'spec-version': 1.6,
|
|
60
|
-
specVersion: 1.6,
|
|
61
|
-
'usages-slices-file': 'usages.slices.json',
|
|
62
|
-
usagesSlicesFile: 'usages.slices.json',
|
|
63
|
-
validate: true,
|
|
64
|
-
...opts,
|
|
65
|
-
};
|
|
66
61
|
const { createBom } = await getCdxGen();
|
|
67
|
-
const sbom = await createBom?.(directory,
|
|
68
|
-
|
|
62
|
+
const sbom = await createBom?.(directory, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
63
|
+
debugLogger('Successfully generated SBOM');
|
|
69
64
|
return sbom?.bomJson;
|
|
70
65
|
}
|
|
71
66
|
// use a value holder, for easier mocking
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NesApolloClient } from "../../api/nes/nes.client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { debugLogger } from "../../service/log.svc.js";
|
|
3
3
|
import { getDaysEolFromEolAt, getStatusFromComponent } from "../line.svc.js";
|
|
4
4
|
import { extractPurls } from "../purls.svc.js";
|
|
5
5
|
import { createBomFromDir } from "./cdx.svc.js";
|
|
@@ -7,7 +7,7 @@ export async function createSbom(directory, opts = {}) {
|
|
|
7
7
|
const sbom = await createBomFromDir(directory, opts.cdxgen || {});
|
|
8
8
|
if (!sbom)
|
|
9
9
|
throw new Error('SBOM not generated');
|
|
10
|
-
|
|
10
|
+
debugLogger('SBOM generated');
|
|
11
11
|
return sbom;
|
|
12
12
|
}
|
|
13
13
|
export function validateIsCycloneDxSbom(sbom) {
|
|
@@ -60,7 +60,7 @@ export async function prepareRows(purls, scan) {
|
|
|
60
60
|
if (!details) {
|
|
61
61
|
// In this case, the purl string is in the generated sbom, but the NES/XEOL api has no data
|
|
62
62
|
// TODO: add UNKNOWN Component Status, create new line, and create flag to show/hide unknown results
|
|
63
|
-
|
|
63
|
+
debugLogger(`Unknown status: ${purl}.`);
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
const { info } = details;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createBom } from '@cyclonedx/cdxgen';
|
|
4
|
+
import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
|
|
5
|
+
process.on('uncaughtException', (err) => {
|
|
6
|
+
console.error('Uncaught exception:', err.message);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
9
|
+
process.on('unhandledRejection', (reason) => {
|
|
10
|
+
console.error('Unhandled rejection:', reason);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
console.log('Sbom worker started');
|
|
15
|
+
const options = JSON.parse(process.argv[2]);
|
|
16
|
+
const { path, opts } = options;
|
|
17
|
+
const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
18
|
+
const outputPath = join(path, 'nes.sbom.json');
|
|
19
|
+
writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('Error creating SBOM', error.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
1
2
|
/**
|
|
2
|
-
* A simple
|
|
3
|
-
*
|
|
3
|
+
* A simple debug logger for services.
|
|
4
|
+
* Services should only use debug logging for development/troubleshooting.
|
|
5
|
+
* All user-facing output should be handled by commands.
|
|
4
6
|
*/
|
|
5
|
-
export declare const
|
|
6
|
-
info: (_message?: unknown, ...args: unknown[]) => void;
|
|
7
|
-
warn: (_message?: unknown, ...args: unknown[]) => void;
|
|
8
|
-
debug: (_message?: unknown, ...args: unknown[]) => void;
|
|
9
|
-
};
|
|
10
|
-
export declare const initOclifLog: (info: (message?: unknown, ...args: unknown[]) => void, warn: (message?: unknown, ...args: unknown[]) => void, debug: (message?: unknown, ...args: unknown[]) => void) => void;
|
|
7
|
+
export declare const debugLogger: debug.Debugger;
|
package/dist/service/log.svc.js
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
1
2
|
/**
|
|
2
|
-
* A simple
|
|
3
|
-
*
|
|
3
|
+
* A simple debug logger for services.
|
|
4
|
+
* Services should only use debug logging for development/troubleshooting.
|
|
5
|
+
* All user-facing output should be handled by commands.
|
|
4
6
|
*/
|
|
5
|
-
export const
|
|
6
|
-
info: (_message, ...args) => {
|
|
7
|
-
console.log('[default_log]', ...args);
|
|
8
|
-
},
|
|
9
|
-
warn: (_message, ...args) => {
|
|
10
|
-
console.warn('[default_warn]', ...args);
|
|
11
|
-
},
|
|
12
|
-
debug: (_message, ...args) => {
|
|
13
|
-
console.debug('[default_debug]', ...args);
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
export const initOclifLog = (info, warn, debug) => {
|
|
17
|
-
log.info = info;
|
|
18
|
-
log.warn = warn;
|
|
19
|
-
log.debug = debug;
|
|
20
|
-
};
|
|
7
|
+
export const debugLogger = debug('oclif:herodevs-debug');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M_SCAN } from "../../api/queries/nes/sbom.js";
|
|
2
|
-
import {
|
|
2
|
+
import { debugLogger } from "../log.svc.js";
|
|
3
3
|
export const buildScanResult = (scan) => {
|
|
4
4
|
const components = new Map();
|
|
5
5
|
for (const c of scan.components) {
|
|
@@ -16,8 +16,8 @@ export const SbomScanner = (client) => async (purls) => {
|
|
|
16
16
|
const res = await client.mutate(M_SCAN.gql, { input });
|
|
17
17
|
const scan = res.data?.insights?.scan?.eol;
|
|
18
18
|
if (!scan?.success) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
debugLogger('failed scan %o', scan || {});
|
|
20
|
+
debugLogger('scan failed');
|
|
21
21
|
throw new Error('Failed to provide scan: ');
|
|
22
22
|
}
|
|
23
23
|
const result = buildScanResult(scan);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0-beta.1",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -19,6 +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
23
|
"format": "biome format --write",
|
|
23
24
|
"lint": "biome lint --write",
|
|
24
25
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"oclif": "^4",
|
|
53
54
|
"shx": "^0.3.3",
|
|
54
55
|
"sinon": "^19.0.2",
|
|
56
|
+
"ts-node": "^10",
|
|
55
57
|
"typescript": "^5.8.0"
|
|
56
58
|
},
|
|
57
59
|
"engines": {
|
|
@@ -70,15 +72,12 @@
|
|
|
70
72
|
"commands": "./dist/commands",
|
|
71
73
|
"plugins": [
|
|
72
74
|
"@oclif/plugin-help",
|
|
73
|
-
"@oclif/plugin-plugins"
|
|
74
|
-
"@oclif/plugin-update"
|
|
75
|
+
"@oclif/plugin-plugins"
|
|
75
76
|
],
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
}
|
|
77
|
+
"hooks": {
|
|
78
|
+
"prerun": "./dist/hooks/prerun.js"
|
|
79
|
+
},
|
|
80
|
+
"topicSeparator": " "
|
|
82
81
|
},
|
|
83
82
|
"types": "dist/index.d.ts"
|
|
84
83
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { initOclifLog, log } from "../../service/log.svc.js";
|
|
2
|
-
const hook = async (opts) => {
|
|
3
|
-
initOclifLog(opts.context.log, opts.context.log, opts.context.debug);
|
|
4
|
-
log.info = opts.context.log || log.info;
|
|
5
|
-
log.warn = opts.context.log || log.warn;
|
|
6
|
-
log.debug = opts.context.debug || log.debug;
|
|
7
|
-
};
|
|
8
|
-
export default hook;
|
|
File without changes
|