@herodevs/cli 1.0.0-beta.2 → 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.
- package/README.md +30 -325
- package/bin/dev.js +6 -6
- package/bin/run.js +3 -2
- package/dist/api/client.d.ts +0 -2
- package/dist/api/client.js +19 -18
- package/dist/api/nes/nes.client.d.ts +4 -0
- package/dist/api/nes/nes.client.js +11 -0
- package/dist/api/queries/nes/sbom.js +5 -0
- package/dist/api/types/nes.types.d.ts +17 -3
- package/dist/api/types/nes.types.js +11 -1
- package/dist/commands/report/committers.d.ts +3 -2
- package/dist/commands/report/committers.js +75 -33
- package/dist/commands/report/purls.d.ts +4 -2
- package/dist/commands/report/purls.js +51 -31
- package/dist/commands/scan/eol.d.ts +13 -4
- package/dist/commands/scan/eol.js +112 -37
- package/dist/commands/scan/sbom.d.ts +4 -1
- package/dist/commands/scan/sbom.js +86 -33
- package/dist/hooks/prerun.js +8 -0
- package/dist/service/committers.svc.js +24 -3
- package/dist/service/eol/cdx.svc.d.ts +52 -0
- package/dist/service/eol/cdx.svc.js +58 -62
- package/dist/service/eol/eol.svc.d.ts +0 -21
- package/dist/service/eol/eol.svc.js +2 -62
- package/dist/service/eol/sbom.worker.d.ts +1 -0
- package/dist/service/eol/sbom.worker.js +26 -0
- package/dist/service/error.svc.d.ts +8 -0
- package/dist/service/error.svc.js +28 -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 +4 -3
- package/dist/service/purls.svc.js +1 -1
- package/dist/ui/date.ui.d.ts +1 -0
- package/dist/ui/date.ui.js +15 -0
- package/dist/ui/eol.ui.d.ts +4 -3
- package/dist/ui/eol.ui.js +56 -15
- package/dist/ui/shared.us.d.ts +3 -0
- package/dist/ui/shared.us.js +13 -0
- package/package.json +13 -14
- 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/service/line.svc.d.ts +0 -24
- package/dist/service/line.svc.js +0 -61
- /package/dist/hooks/{prerun/CommandContextHook.d.ts → prerun.d.ts} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
6
|
+
import { getErrorMessage } from "../../service/error.svc.js";
|
|
5
7
|
export default class ScanSbom extends Command {
|
|
6
8
|
static description = 'Scan a SBOM for purls';
|
|
7
9
|
static enableJsonFlag = true;
|
|
@@ -23,16 +25,33 @@ export default class ScanSbom extends Command {
|
|
|
23
25
|
default: false,
|
|
24
26
|
description: 'Save the generated SBOM as nes.sbom.json in the scanned directory',
|
|
25
27
|
}),
|
|
28
|
+
background: Flags.boolean({
|
|
29
|
+
char: 'b',
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'Run the scan in the background',
|
|
32
|
+
}),
|
|
26
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
|
+
}
|
|
27
43
|
static getSbomArgs(flags) {
|
|
28
|
-
const { dir, file, save } = flags ?? {};
|
|
44
|
+
const { dir, file, save, json, background } = flags ?? {};
|
|
29
45
|
const sbomArgs = [];
|
|
30
46
|
if (file)
|
|
31
47
|
sbomArgs.push('--file', file);
|
|
32
48
|
if (dir)
|
|
33
49
|
sbomArgs.push('--dir', dir);
|
|
34
|
-
if (save)
|
|
35
|
-
|
|
50
|
+
// if (save) sbomArgs.push('--save'); // only save if sbom command is used directly with -s flag
|
|
51
|
+
if (json)
|
|
52
|
+
sbomArgs.push('--json');
|
|
53
|
+
if (background)
|
|
54
|
+
sbomArgs.push('--background');
|
|
36
55
|
return sbomArgs;
|
|
37
56
|
}
|
|
38
57
|
getScanOptions() {
|
|
@@ -41,44 +60,78 @@ export default class ScanSbom extends Command {
|
|
|
41
60
|
}
|
|
42
61
|
async run() {
|
|
43
62
|
const { flags } = await this.parse(ScanSbom);
|
|
44
|
-
const { dir
|
|
63
|
+
const { dir, save, file, background } = flags;
|
|
45
64
|
// Validate that exactly one of --file or --dir is provided
|
|
46
|
-
if (
|
|
47
|
-
|
|
65
|
+
if (file && dir) {
|
|
66
|
+
this.error('Cannot specify both --file and --dir flags. Please use one or the other.');
|
|
48
67
|
}
|
|
49
68
|
let sbom;
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
const path = dir || process.cwd();
|
|
70
|
+
if (file) {
|
|
71
|
+
sbom = this._getSbomFromFile(file);
|
|
72
|
+
}
|
|
73
|
+
else if (background) {
|
|
74
|
+
this._getSbomInBackground(path);
|
|
75
|
+
this.log(`The scan is running in the background. The file will be saved at ${path}/nes.sbom.json`);
|
|
76
|
+
return;
|
|
52
77
|
}
|
|
53
78
|
else {
|
|
54
|
-
|
|
55
|
-
sbom = await this._getSbomFromScan(_dir);
|
|
79
|
+
sbom = await this._getSbomFromScan(path);
|
|
56
80
|
if (save) {
|
|
57
|
-
this._saveSbom(
|
|
81
|
+
this._saveSbom(path, sbom);
|
|
58
82
|
}
|
|
59
83
|
}
|
|
60
84
|
return sbom;
|
|
61
85
|
}
|
|
62
86
|
async _getSbomFromScan(_dirFlag) {
|
|
63
|
-
const dir =
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
const dir = resolve(_dirFlag);
|
|
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;
|
|
66
103
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const sbom = await createSbom(dir, options);
|
|
70
|
-
if (!sbom) {
|
|
71
|
-
throw new Error(`SBOM failed to generate for dir: ${dir}`);
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.error(`Failed to scan directory: ${getErrorMessage(error)}`);
|
|
72
106
|
}
|
|
73
|
-
return sbom;
|
|
74
107
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
_getSbomInBackground(path) {
|
|
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();
|
|
79
123
|
}
|
|
80
|
-
|
|
124
|
+
catch (error) {
|
|
125
|
+
this.error(`Failed to start background scan: ${getErrorMessage(error)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
_getSbomFromFile(_fileFlag) {
|
|
129
|
+
const file = resolve(_fileFlag);
|
|
81
130
|
try {
|
|
131
|
+
if (!fs.existsSync(file)) {
|
|
132
|
+
this.error(`SBOM file not found: ${file}`);
|
|
133
|
+
}
|
|
134
|
+
ux.action.start(`Loading sbom from ${file}`);
|
|
82
135
|
const fileContent = fs.readFileSync(file, {
|
|
83
136
|
encoding: 'utf8',
|
|
84
137
|
flag: 'r',
|
|
@@ -88,19 +141,19 @@ export default class ScanSbom extends Command {
|
|
|
88
141
|
return sbom;
|
|
89
142
|
}
|
|
90
143
|
catch (error) {
|
|
91
|
-
|
|
92
|
-
throw new Error(`Failed to read or parse SBOM file: ${errorMessage}`);
|
|
144
|
+
this.error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
|
|
93
145
|
}
|
|
94
146
|
}
|
|
95
147
|
_saveSbom(dir, sbom) {
|
|
96
148
|
try {
|
|
97
|
-
const outputPath =
|
|
149
|
+
const outputPath = join(dir, 'nes.sbom.json');
|
|
98
150
|
fs.writeFileSync(outputPath, JSON.stringify(sbom, null, 2));
|
|
99
|
-
this.
|
|
151
|
+
if (!this.jsonEnabled()) {
|
|
152
|
+
this.log(`SBOM saved to ${outputPath}`);
|
|
153
|
+
}
|
|
100
154
|
}
|
|
101
155
|
catch (error) {
|
|
102
|
-
|
|
103
|
-
this.warn(`Failed to save SBOM: ${errorMessage}`);
|
|
156
|
+
this.error(`Failed to save SBOM: ${getErrorMessage(error)}`);
|
|
104
157
|
}
|
|
105
158
|
}
|
|
106
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 =
|
|
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 =
|
|
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 =
|
|
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) {
|
|
@@ -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
|
|
@@ -77,6 +72,7 @@ export async function getCdxGen() {
|
|
|
77
72
|
const ogEnv = process.env.NODE_ENV;
|
|
78
73
|
process.env.NODE_ENV = undefined;
|
|
79
74
|
try {
|
|
75
|
+
// @ts-expect-error
|
|
80
76
|
cdxgen.createBom = (await import('@cyclonedx/cdxgen')).createBom;
|
|
81
77
|
}
|
|
82
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,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { log } from "../../service/log.svc.js";
|
|
3
|
-
import { getDaysEolFromEolAt, getStatusFromComponent } from "../line.svc.js";
|
|
4
|
-
import { extractPurls } from "../purls.svc.js";
|
|
1
|
+
import { debugLogger } from "../../service/log.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 || {});
|
|
8
5
|
if (!sbom)
|
|
9
6
|
throw new Error('SBOM not generated');
|
|
10
|
-
|
|
7
|
+
debugLogger('SBOM generated');
|
|
11
8
|
return sbom;
|
|
12
9
|
}
|
|
13
10
|
export function validateIsCycloneDxSbom(sbom) {
|
|
@@ -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
|
-
log.debug(`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";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// @ts-expect-error
|
|
4
|
+
import { createBom } from '@cyclonedx/cdxgen';
|
|
5
|
+
import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
|
|
6
|
+
process.on('uncaughtException', (err) => {
|
|
7
|
+
console.error('Uncaught exception:', err.message);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
process.on('unhandledRejection', (reason) => {
|
|
11
|
+
console.error('Unhandled rejection:', reason);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
try {
|
|
15
|
+
console.log('Sbom worker started');
|
|
16
|
+
const options = JSON.parse(process.argv[2]);
|
|
17
|
+
const { path, opts } = options;
|
|
18
|
+
const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
19
|
+
const outputPath = join(path, 'nes.sbom.json');
|
|
20
|
+
writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('Error creating SBOM', error.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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) {
|
|
@@ -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) => {
|
|
@@ -16,8 +17,8 @@ export const SbomScanner = (client) => async (purls) => {
|
|
|
16
17
|
const res = await client.mutate(M_SCAN.gql, { input });
|
|
17
18
|
const scan = res.data?.insights?.scan?.eol;
|
|
18
19
|
if (!scan?.success) {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
debugLogger('failed scan %o', scan || {});
|
|
21
|
+
debugLogger('scan failed');
|
|
21
22
|
throw new Error('Failed to provide scan: ');
|
|
22
23
|
}
|
|
23
24
|
const result = buildScanResult(scan);
|