@herodevs/cli 2.0.0-beta.4 → 2.0.0-beta.5
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 +142 -108
- package/dist/api/gql-operations.d.ts +2 -0
- package/dist/api/gql-operations.js +36 -0
- package/dist/api/nes.client.d.ts +12 -0
- package/dist/api/nes.client.js +71 -0
- package/dist/commands/scan/eol.d.ts +12 -12
- package/dist/commands/scan/eol.js +163 -104
- package/dist/config/constants.d.ts +8 -3
- package/dist/config/constants.js +18 -3
- package/dist/hooks/finally.d.ts +3 -0
- package/dist/hooks/finally.js +14 -0
- package/dist/hooks/prerun.js +12 -0
- package/dist/service/analytics.svc.d.ts +28 -0
- package/dist/service/analytics.svc.js +112 -0
- package/dist/service/{eol/cdx.svc.d.ts → cdx.svc.d.ts} +8 -16
- package/dist/service/{eol/cdx.svc.js → cdx.svc.js} +17 -7
- package/dist/service/display.svc.d.ts +22 -0
- package/dist/service/display.svc.js +72 -0
- package/dist/service/file.svc.d.ts +20 -0
- package/dist/service/file.svc.js +71 -0
- package/dist/service/log.svc.d.ts +1 -0
- package/dist/service/log.svc.js +9 -0
- package/dist/service/{eol/sbom.worker.js → sbom.worker.js} +1 -1
- package/package.json +22 -15
- package/dist/api/client.d.ts +0 -12
- package/dist/api/client.js +0 -43
- package/dist/api/nes/nes.client.d.ts +0 -24
- package/dist/api/nes/nes.client.js +0 -127
- package/dist/api/queries/nes/sbom.d.ts +0 -3
- package/dist/api/queries/nes/sbom.js +0 -39
- package/dist/api/queries/nes/telemetry.d.ts +0 -2
- package/dist/api/queries/nes/telemetry.js +0 -24
- package/dist/api/types/hd-cli.types.d.ts +0 -30
- package/dist/api/types/hd-cli.types.js +0 -10
- package/dist/api/types/nes.types.d.ts +0 -58
- package/dist/api/types/nes.types.js +0 -1
- package/dist/commands/report/committers.d.ts +0 -23
- package/dist/commands/report/committers.js +0 -147
- package/dist/commands/report/purls.d.ts +0 -15
- package/dist/commands/report/purls.js +0 -85
- package/dist/commands/scan/sbom.d.ts +0 -21
- package/dist/commands/scan/sbom.js +0 -164
- package/dist/service/committers.svc.d.ts +0 -70
- package/dist/service/committers.svc.js +0 -196
- package/dist/service/eol/eol.svc.d.ts +0 -12
- package/dist/service/eol/eol.svc.js +0 -25
- package/dist/service/error.svc.d.ts +0 -8
- package/dist/service/error.svc.js +0 -28
- package/dist/service/nes/nes.svc.d.ts +0 -5
- package/dist/service/nes/nes.svc.js +0 -36
- package/dist/service/purls.svc.d.ts +0 -23
- package/dist/service/purls.svc.js +0 -99
- package/dist/ui/shared.ui.d.ts +0 -4
- package/dist/ui/shared.ui.js +0 -14
- /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentStatus, EolReport } from '@herodevs/eol-shared';
|
|
2
|
+
/**
|
|
3
|
+
* Formats status row text with appropriate color and icon
|
|
4
|
+
*/
|
|
5
|
+
export declare const getStatusRowText: Record<ComponentStatus, (text: string) => string>;
|
|
6
|
+
export type ComponentCounts = Record<ComponentStatus, number> & {
|
|
7
|
+
NES_AVAILABLE: number;
|
|
8
|
+
TOTAL: number;
|
|
9
|
+
ECOSYSTEMS: string[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Counts components by their status, including NES remediation availability
|
|
13
|
+
*/
|
|
14
|
+
export declare function countComponentsByStatus(report: EolReport): ComponentCounts;
|
|
15
|
+
/**
|
|
16
|
+
* Formats scan results for console display
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatScanResults(report: EolReport): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Formats web report URL for console display
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatWebReportUrl(id: string, reportCardUrl: string): string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { deriveComponentStatus } from '@herodevs/eol-shared';
|
|
2
|
+
import { ux } from '@oclif/core';
|
|
3
|
+
import terminalLink from 'terminal-link';
|
|
4
|
+
const STATUS_COLORS = {
|
|
5
|
+
EOL: 'red',
|
|
6
|
+
UNKNOWN: 'default',
|
|
7
|
+
OK: 'green',
|
|
8
|
+
EOL_UPCOMING: 'yellow',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Formats status row text with appropriate color and icon
|
|
12
|
+
*/
|
|
13
|
+
export const getStatusRowText = {
|
|
14
|
+
EOL: (text) => ux.colorize(STATUS_COLORS.EOL, `✗ ${text}`),
|
|
15
|
+
UNKNOWN: (text) => ux.colorize(STATUS_COLORS.UNKNOWN, `• ${text}`),
|
|
16
|
+
OK: (text) => ux.colorize(STATUS_COLORS.OK, `✔ ${text}`),
|
|
17
|
+
EOL_UPCOMING: (text) => ux.colorize(STATUS_COLORS.EOL_UPCOMING, `! ${text}`),
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Counts components by their status, including NES remediation availability
|
|
21
|
+
*/
|
|
22
|
+
export function countComponentsByStatus(report) {
|
|
23
|
+
const grouped = {
|
|
24
|
+
UNKNOWN: 0,
|
|
25
|
+
OK: 0,
|
|
26
|
+
EOL_UPCOMING: 0,
|
|
27
|
+
EOL: 0,
|
|
28
|
+
NES_AVAILABLE: 0,
|
|
29
|
+
ECOSYSTEMS: [],
|
|
30
|
+
TOTAL: report.components.length,
|
|
31
|
+
};
|
|
32
|
+
const ecosystems = new Set();
|
|
33
|
+
for (const component of report.components) {
|
|
34
|
+
const status = deriveComponentStatus(component.metadata);
|
|
35
|
+
grouped[status]++;
|
|
36
|
+
if (component.nesRemediation?.remediations?.length) {
|
|
37
|
+
grouped.NES_AVAILABLE++;
|
|
38
|
+
}
|
|
39
|
+
const ecosystem = component.purl.match(/^pkg:([^/]+)\//)?.[1];
|
|
40
|
+
if (ecosystem) {
|
|
41
|
+
ecosystems.add(ecosystem);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
grouped.ECOSYSTEMS = Array.from(ecosystems);
|
|
45
|
+
return grouped;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Formats scan results for console display
|
|
49
|
+
*/
|
|
50
|
+
export function formatScanResults(report) {
|
|
51
|
+
const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(report);
|
|
52
|
+
if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
|
|
53
|
+
return [ux.colorize('yellow', 'No components found in scan.')];
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
ux.colorize('bold', 'Scan results:'),
|
|
57
|
+
ux.colorize('bold', '-'.repeat(40)),
|
|
58
|
+
ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`),
|
|
59
|
+
getStatusRowText.EOL(`${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`),
|
|
60
|
+
getStatusRowText.EOL_UPCOMING(`${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`),
|
|
61
|
+
getStatusRowText.OK(`${OK.toLocaleString().padEnd(5)} Not End-of-Life`),
|
|
62
|
+
getStatusRowText.UNKNOWN(`${UNKNOWN.toLocaleString().padEnd(5)} Unknown Status`),
|
|
63
|
+
getStatusRowText.UNKNOWN(`${NES_AVAILABLE.toLocaleString().padEnd(5)} HeroDevs NES Remediation${NES_AVAILABLE !== 1 ? 's' : ''} Available`),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Formats web report URL for console display
|
|
68
|
+
*/
|
|
69
|
+
export function formatWebReportUrl(id, reportCardUrl) {
|
|
70
|
+
const url = ux.colorize('blue', terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }));
|
|
71
|
+
return [ux.colorize('bold', '-'.repeat(40)), `🌐 View your full EOL report at: ${url}\n`];
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CdxBom, EolReport } from '@herodevs/eol-shared';
|
|
2
|
+
export interface FileError extends Error {
|
|
3
|
+
code?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Reads an SBOM from a file path
|
|
7
|
+
*/
|
|
8
|
+
export declare function readSbomFromFile(filePath: string): CdxBom;
|
|
9
|
+
/**
|
|
10
|
+
* Validates that a directory path exists and is actually a directory
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateDirectory(dirPath: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Saves an SBOM to a file in the specified directory
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveSbomToFile(dir: string, sbom: CdxBom): string;
|
|
17
|
+
/**
|
|
18
|
+
* Saves an EOL report to a file in the specified directory
|
|
19
|
+
*/
|
|
20
|
+
export declare function saveReportToFile(dir: string, report: EolReport): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path, { join, resolve } from 'node:path';
|
|
3
|
+
import { isCdxBom } from '@herodevs/eol-shared';
|
|
4
|
+
import { filenamePrefix } from "../config/constants.js";
|
|
5
|
+
import { getErrorMessage } from "./log.svc.js";
|
|
6
|
+
/**
|
|
7
|
+
* Reads an SBOM from a file path
|
|
8
|
+
*/
|
|
9
|
+
export function readSbomFromFile(filePath) {
|
|
10
|
+
const file = resolve(filePath);
|
|
11
|
+
if (!fs.existsSync(file)) {
|
|
12
|
+
throw new Error(`SBOM file not found: ${file}`);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const fileContent = fs.readFileSync(file, 'utf8');
|
|
16
|
+
const sbom = JSON.parse(fileContent);
|
|
17
|
+
if (!isCdxBom(sbom)) {
|
|
18
|
+
throw new Error(`Invalid SBOM file: ${file}`);
|
|
19
|
+
}
|
|
20
|
+
return sbom;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validates that a directory path exists and is actually a directory
|
|
28
|
+
*/
|
|
29
|
+
export function validateDirectory(dirPath) {
|
|
30
|
+
const dir = resolve(dirPath);
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
33
|
+
}
|
|
34
|
+
const stats = fs.statSync(dir);
|
|
35
|
+
if (!stats.isDirectory()) {
|
|
36
|
+
throw new Error(`Path is not a directory: ${dir}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Saves an SBOM to a file in the specified directory
|
|
41
|
+
*/
|
|
42
|
+
export function saveSbomToFile(dir, sbom) {
|
|
43
|
+
const outputPath = join(dir, `${filenamePrefix}.sbom.json`);
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(outputPath, JSON.stringify(sbom, null, 2));
|
|
46
|
+
return outputPath;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`Failed to save SBOM: ${getErrorMessage(error)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Saves an EOL report to a file in the specified directory
|
|
54
|
+
*/
|
|
55
|
+
export function saveReportToFile(dir, report) {
|
|
56
|
+
const reportPath = path.join(dir, `${filenamePrefix}.report.json`);
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
59
|
+
return reportPath;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const fileError = error;
|
|
63
|
+
if (fileError.code === 'EACCES') {
|
|
64
|
+
throw new Error(`Permission denied. Unable to save report to ${filenamePrefix}.report.json`);
|
|
65
|
+
}
|
|
66
|
+
if (fileError.code === 'ENOSPC') {
|
|
67
|
+
throw new Error(`No space left on device. Unable to save report to ${filenamePrefix}.report.json`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Failed to save report: ${getErrorMessage(error)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/service/log.svc.js
CHANGED
|
@@ -5,3 +5,12 @@ import debug from 'debug';
|
|
|
5
5
|
* All user-facing output should be handled by commands.
|
|
6
6
|
*/
|
|
7
7
|
export const debugLogger = debug('oclif:herodevs-debug');
|
|
8
|
+
export function getErrorMessage(error) {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
return error.message;
|
|
11
|
+
}
|
|
12
|
+
if (error && typeof error === 'object') {
|
|
13
|
+
return JSON.stringify(error);
|
|
14
|
+
}
|
|
15
|
+
return String(error) || 'Unknown error';
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { createBom } from '@cyclonedx/cdxgen';
|
|
4
|
-
import { filenamePrefix } from "
|
|
4
|
+
import { filenamePrefix } from "../config/constants.js";
|
|
5
5
|
import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
|
|
6
6
|
process.on('uncaughtException', (err) => {
|
|
7
7
|
console.error('Uncaught exception:', err.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herodevs/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.5",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
"format": "biome format --write",
|
|
24
24
|
"lint": "biome lint --write",
|
|
25
25
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
26
|
+
"prepare": "npm run build",
|
|
26
27
|
"prepack": "oclif manifest && oclif readme",
|
|
27
28
|
"pretest": "npm run lint && npm run typecheck",
|
|
28
29
|
"readme": "npm run ci:fix && npm run build && npm exec oclif readme",
|
|
29
|
-
"test": "globstar -- node --import tsx --test \"test/**/*.test.ts\"",
|
|
30
|
+
"test": "globstar -- node --import tsx --test --experimental-test-module-mocks \"test/**/*.test.ts\"",
|
|
30
31
|
"test:e2e": "globstar -- node --import tsx --test \"e2e/**/*.test.ts\"",
|
|
31
|
-
"typecheck": "tsc --noEmit"
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"version": "oclif manifest && npm run readme"
|
|
32
34
|
},
|
|
33
35
|
"keywords": [
|
|
34
36
|
"herodevs",
|
|
@@ -36,30 +38,34 @@
|
|
|
36
38
|
"herodevs cli"
|
|
37
39
|
],
|
|
38
40
|
"dependencies": {
|
|
41
|
+
"@amplitude/analytics-node": "^1.5.5",
|
|
39
42
|
"@apollo/client": "^3.13.8",
|
|
40
|
-
"@cyclonedx/cdxgen": "
|
|
41
|
-
"@
|
|
42
|
-
"@oclif/
|
|
43
|
-
"@oclif/plugin-
|
|
43
|
+
"@cyclonedx/cdxgen": "~11.4.4",
|
|
44
|
+
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.10",
|
|
45
|
+
"@oclif/core": "^4.5.2",
|
|
46
|
+
"@oclif/plugin-help": "^6.2.32",
|
|
47
|
+
"@oclif/plugin-update": "^4.7.4",
|
|
44
48
|
"graphql": "^16.11.0",
|
|
49
|
+
"node-machine-id": "^1.1.12",
|
|
50
|
+
"ora": "^8.2.0",
|
|
45
51
|
"packageurl-js": "^2.0.1",
|
|
46
52
|
"terminal-link": "^4.0.0",
|
|
47
53
|
"update-notifier": "^7.3.1"
|
|
48
54
|
},
|
|
49
55
|
"devDependencies": {
|
|
50
|
-
"@biomejs/biome": "^
|
|
56
|
+
"@biomejs/biome": "^2.2.2",
|
|
51
57
|
"@oclif/test": "^4.1.13",
|
|
52
|
-
"@types/inquirer": "^9.0.
|
|
53
|
-
"@types/node": "^
|
|
58
|
+
"@types/inquirer": "^9.0.9",
|
|
59
|
+
"@types/node": "^24.3.0",
|
|
54
60
|
"@types/sinon": "^17.0.4",
|
|
55
61
|
"@types/update-notifier": "^6.0.8",
|
|
56
62
|
"globstar": "^1.0.0",
|
|
57
|
-
"oclif": "^4.
|
|
63
|
+
"oclif": "^4.22.14",
|
|
58
64
|
"shx": "^0.4.0",
|
|
59
|
-
"sinon": "^
|
|
65
|
+
"sinon": "^21.0.0",
|
|
60
66
|
"ts-node": "^10.9.2",
|
|
61
|
-
"tsx": "^4.20.
|
|
62
|
-
"typescript": "^5.
|
|
67
|
+
"tsx": "^4.20.5",
|
|
68
|
+
"typescript": "^5.9.2"
|
|
63
69
|
},
|
|
64
70
|
"engines": {
|
|
65
71
|
"node": ">=20.0.0"
|
|
@@ -82,7 +88,8 @@
|
|
|
82
88
|
],
|
|
83
89
|
"hooks": {
|
|
84
90
|
"init": "./dist/hooks/npm-update-notifier.js",
|
|
85
|
-
"prerun": "./dist/hooks/prerun.js"
|
|
91
|
+
"prerun": "./dist/hooks/prerun.js",
|
|
92
|
+
"finally": "./dist/hooks/finally.js"
|
|
86
93
|
},
|
|
87
94
|
"topicSeparator": " ",
|
|
88
95
|
"macos": {
|
package/dist/api/client.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
export interface ApolloHelper {
|
|
3
|
-
mutate<T, V extends apollo.OperationVariables>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
4
|
-
query<T, V extends apollo.OperationVariables | undefined = undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
5
|
-
}
|
|
6
|
-
export declare const createApollo: (url: string) => apollo.ApolloClient<apollo.NormalizedCacheObject>;
|
|
7
|
-
export declare class ApolloClient implements ApolloHelper {
|
|
8
|
-
#private;
|
|
9
|
-
constructor(url: string);
|
|
10
|
-
mutate<T, V extends apollo.OperationVariables>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
11
|
-
query<T, V extends apollo.OperationVariables | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
12
|
-
}
|
package/dist/api/client.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
import { ApolloError } from "../service/error.svc.js";
|
|
3
|
-
export const createApollo = (url) => new apollo.ApolloClient({
|
|
4
|
-
cache: new apollo.InMemoryCache({
|
|
5
|
-
addTypename: false,
|
|
6
|
-
}),
|
|
7
|
-
headers: {
|
|
8
|
-
'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`,
|
|
9
|
-
},
|
|
10
|
-
link: apollo.ApolloLink.from([
|
|
11
|
-
new apollo.HttpLink({
|
|
12
|
-
uri: url,
|
|
13
|
-
}),
|
|
14
|
-
]),
|
|
15
|
-
});
|
|
16
|
-
export class ApolloClient {
|
|
17
|
-
#apollo;
|
|
18
|
-
constructor(url) {
|
|
19
|
-
this.#apollo = createApollo(url);
|
|
20
|
-
}
|
|
21
|
-
async mutate(mutation, variables) {
|
|
22
|
-
try {
|
|
23
|
-
return await this.#apollo.mutate({
|
|
24
|
-
mutation,
|
|
25
|
-
variables,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
throw new ApolloError('GraphQL mutation failed', error);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async query(query, variables) {
|
|
33
|
-
try {
|
|
34
|
-
return await this.#apollo.query({
|
|
35
|
-
query,
|
|
36
|
-
variables,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
throw new ApolloError('GraphQL query failed', error);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
import type { InsightsEolScanInput, InsightsEolScanResult } from '../../api/types/nes.types.ts';
|
|
3
|
-
import { type ProcessBatchOptions, type ScanInputOptions, type ScanResult } from '../types/hd-cli.types.ts';
|
|
4
|
-
export interface NesClient {
|
|
5
|
-
scan: {
|
|
6
|
-
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export declare class NesApolloClient implements NesClient {
|
|
10
|
-
#private;
|
|
11
|
-
scan: {
|
|
12
|
-
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
13
|
-
};
|
|
14
|
-
constructor(url: string);
|
|
15
|
-
mutate<T, V extends Record<string, unknown>>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
16
|
-
query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
17
|
-
}
|
|
18
|
-
export declare const batchSubmitPurls: (purls: string[], options?: ScanInputOptions, batchSize?: number) => Promise<ScanResult>;
|
|
19
|
-
export declare const dedupeAndEncodePurls: (purls: string[]) => string[];
|
|
20
|
-
export declare const createBatches: (items: string[], batchSize: number) => string[][];
|
|
21
|
-
export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
|
|
22
|
-
export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
|
|
23
|
-
export declare const handleBatchResults: (results: InsightsEolScanResult[]) => ScanResult;
|
|
24
|
-
export declare const buildInsightsEolScanInput: (purls: string[], options: ScanInputOptions) => InsightsEolScanInput;
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { PackageURL } from 'packageurl-js';
|
|
2
|
-
import { ApolloClient } from "../../api/client.js";
|
|
3
|
-
import { config } from "../../config/constants.js";
|
|
4
|
-
import { debugLogger } from "../../service/log.svc.js";
|
|
5
|
-
import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
|
|
6
|
-
import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS, } from "../types/hd-cli.types.js";
|
|
7
|
-
export class NesApolloClient {
|
|
8
|
-
scan = {
|
|
9
|
-
purls: SbomScanner(this),
|
|
10
|
-
};
|
|
11
|
-
#apollo;
|
|
12
|
-
constructor(url) {
|
|
13
|
-
this.#apollo = new ApolloClient(url);
|
|
14
|
-
}
|
|
15
|
-
mutate(mutation, variables) {
|
|
16
|
-
return this.#apollo.mutate(mutation, variables);
|
|
17
|
-
}
|
|
18
|
-
query(query, variables) {
|
|
19
|
-
return this.#apollo.query(query, variables);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Submit a scan for a list of purls after they've been batched by batchSubmitPurls
|
|
24
|
-
*/
|
|
25
|
-
function submitScan(purls, options) {
|
|
26
|
-
const host = config.graphqlHost;
|
|
27
|
-
const path = config.graphqlPath;
|
|
28
|
-
const url = host + path;
|
|
29
|
-
debugLogger('Submitting scan to %s', url);
|
|
30
|
-
const client = new NesApolloClient(url);
|
|
31
|
-
return client.scan.purls(purls, options);
|
|
32
|
-
}
|
|
33
|
-
export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIONS, batchSize = DEFAULT_SCAN_BATCH_SIZE) => {
|
|
34
|
-
try {
|
|
35
|
-
const dedupedAndEncodedPurls = dedupeAndEncodePurls(purls);
|
|
36
|
-
const batches = createBatches(dedupedAndEncodedPurls, batchSize);
|
|
37
|
-
debugLogger('Processing %d batches', batches.length);
|
|
38
|
-
if (batches.length === 0) {
|
|
39
|
-
return {
|
|
40
|
-
components: new Map(),
|
|
41
|
-
message: 'No batches to process',
|
|
42
|
-
success: true,
|
|
43
|
-
warnings: [],
|
|
44
|
-
scanId: undefined,
|
|
45
|
-
createdOn: undefined,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const results = await processBatches(batches, options);
|
|
49
|
-
return handleBatchResults(results);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
debugLogger('Fatal error in batchSubmitPurls: %s', error);
|
|
53
|
-
throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
export const dedupeAndEncodePurls = (purls) => {
|
|
57
|
-
const dedupedAndEncodedPurls = new Set();
|
|
58
|
-
for (const purl of purls) {
|
|
59
|
-
try {
|
|
60
|
-
// The PackageURL.fromString method encodes each part of the purl
|
|
61
|
-
const encodedPurl = PackageURL.fromString(purl).toString();
|
|
62
|
-
if (!dedupedAndEncodedPurls.has(encodedPurl)) {
|
|
63
|
-
dedupedAndEncodedPurls.add(encodedPurl);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
debugLogger('Error encoding purl: %s', error);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return Array.from(dedupedAndEncodedPurls);
|
|
71
|
-
};
|
|
72
|
-
export const createBatches = (items, batchSize) => {
|
|
73
|
-
const numberOfBatches = Math.ceil(items.length / batchSize);
|
|
74
|
-
return Array.from({ length: numberOfBatches }, (_, index) => {
|
|
75
|
-
const startIndex = index * batchSize;
|
|
76
|
-
const endIndex = startIndex + batchSize;
|
|
77
|
-
return items.slice(startIndex, endIndex);
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
|
-
export const processBatch = async ({ batch, index, totalPages, scanOptions, previousScanId, }) => {
|
|
81
|
-
const page = index + 1;
|
|
82
|
-
if (page > totalPages) {
|
|
83
|
-
throw new Error('Total pages exceeded');
|
|
84
|
-
}
|
|
85
|
-
debugLogger('Processing batch %d of %d', page, totalPages);
|
|
86
|
-
debugLogger('ScanID: %s', previousScanId);
|
|
87
|
-
const result = await submitScan(batch, {
|
|
88
|
-
...scanOptions,
|
|
89
|
-
page,
|
|
90
|
-
totalPages,
|
|
91
|
-
scanId: previousScanId,
|
|
92
|
-
});
|
|
93
|
-
return result;
|
|
94
|
-
};
|
|
95
|
-
export const processBatches = async (batches, scanOptions) => {
|
|
96
|
-
const totalPages = batches.length;
|
|
97
|
-
const results = [];
|
|
98
|
-
for (const [index, batch] of batches.entries()) {
|
|
99
|
-
const previousScanId = results[index - 1]?.scanId;
|
|
100
|
-
const result = await processBatch({
|
|
101
|
-
batch,
|
|
102
|
-
index,
|
|
103
|
-
totalPages,
|
|
104
|
-
scanOptions,
|
|
105
|
-
previousScanId,
|
|
106
|
-
});
|
|
107
|
-
results.push(result);
|
|
108
|
-
}
|
|
109
|
-
return results;
|
|
110
|
-
};
|
|
111
|
-
export const handleBatchResults = (results) => {
|
|
112
|
-
if (results.length === 0) {
|
|
113
|
-
throw new Error('No results to process');
|
|
114
|
-
}
|
|
115
|
-
// The API returns placeholders for each batch except the last one.
|
|
116
|
-
const finalResult = results[results.length - 1];
|
|
117
|
-
return buildScanResult(finalResult);
|
|
118
|
-
};
|
|
119
|
-
export const buildInsightsEolScanInput = (purls, options) => {
|
|
120
|
-
const { type, page, totalPages } = options;
|
|
121
|
-
return {
|
|
122
|
-
components: purls,
|
|
123
|
-
type,
|
|
124
|
-
page,
|
|
125
|
-
totalPages,
|
|
126
|
-
};
|
|
127
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core/core.cjs';
|
|
2
|
-
export const M_SCAN = {
|
|
3
|
-
gql: gql `
|
|
4
|
-
mutation EolScan($input: InsightsEolScanInput!) {
|
|
5
|
-
insights {
|
|
6
|
-
scan {
|
|
7
|
-
eol(input: $input) {
|
|
8
|
-
components {
|
|
9
|
-
purl
|
|
10
|
-
info {
|
|
11
|
-
isEol
|
|
12
|
-
isUnsafe
|
|
13
|
-
eolAt
|
|
14
|
-
daysEol
|
|
15
|
-
status
|
|
16
|
-
vulnCount
|
|
17
|
-
}
|
|
18
|
-
remediation {
|
|
19
|
-
id
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
diagnostics
|
|
23
|
-
message
|
|
24
|
-
scanId
|
|
25
|
-
createdOn
|
|
26
|
-
success
|
|
27
|
-
warnings {
|
|
28
|
-
purl
|
|
29
|
-
type
|
|
30
|
-
message
|
|
31
|
-
error
|
|
32
|
-
diagnostics
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
`,
|
|
39
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core/core.cjs';
|
|
2
|
-
export const TELEMETRY_INITIALIZE_MUTATION = gql `
|
|
3
|
-
mutation Telemetry($clientName: String!) {
|
|
4
|
-
telemetry {
|
|
5
|
-
initialize(input: { context: { client: { id: $clientName } } }) {
|
|
6
|
-
success
|
|
7
|
-
oid
|
|
8
|
-
message
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
`;
|
|
13
|
-
export const TELEMETRY_REPORT_MUTATION = gql `
|
|
14
|
-
mutation Report($key: String!, $report: JSON!, $metadata: JSON) {
|
|
15
|
-
telemetry {
|
|
16
|
-
report(input: { key: $key, report: $report, metadata: $metadata }) {
|
|
17
|
-
txId
|
|
18
|
-
success
|
|
19
|
-
message
|
|
20
|
-
diagnostics
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
@@ -1,30 +0,0 @@
|
|
|
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 ScanInput = {
|
|
12
|
-
components: string[];
|
|
13
|
-
options: ScanInputOptions;
|
|
14
|
-
};
|
|
15
|
-
export interface ScanResult {
|
|
16
|
-
components: Map<string, InsightsEolScanComponent>;
|
|
17
|
-
createdOn?: string;
|
|
18
|
-
diagnostics?: Record<string, unknown>;
|
|
19
|
-
message: string;
|
|
20
|
-
success: boolean;
|
|
21
|
-
warnings: ScanWarning[];
|
|
22
|
-
scanId: string | undefined;
|
|
23
|
-
}
|
|
24
|
-
export interface ProcessBatchOptions {
|
|
25
|
-
batch: string[];
|
|
26
|
-
index: number;
|
|
27
|
-
totalPages: number;
|
|
28
|
-
scanOptions: ScanInputOptions;
|
|
29
|
-
previousScanId?: string;
|
|
30
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
};
|