@herodevs/cli 2.0.0-beta.1 → 2.0.0-beta.11
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 +201 -110
- package/bin/dev.js +1 -4
- package/bin/main.js +3 -3
- package/bin/run.js +1 -1
- 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 +84 -0
- package/dist/commands/scan/eol.d.ts +18 -19
- package/dist/commands/scan/eol.js +214 -142
- package/dist/config/constants.d.ts +9 -3
- package/dist/config/constants.js +19 -3
- package/dist/hooks/finally/finally.d.ts +3 -0
- package/dist/hooks/finally/finally.js +18 -0
- package/dist/hooks/{npm-update-notifier.js → init/00_npm-update-notifier.js} +3 -3
- package/dist/hooks/init/01_initialize_amplitude.d.ts +3 -0
- package/dist/hooks/init/01_initialize_amplitude.js +15 -0
- package/dist/service/analytics.svc.d.ts +27 -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 +30 -0
- package/dist/service/display.svc.js +87 -0
- package/dist/service/file.svc.d.ts +30 -0
- package/dist/service/file.svc.js +115 -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} +2 -1
- package/dist/utils/strip-typename.d.ts +1 -0
- package/dist/utils/strip-typename.js +15 -0
- package/package.json +33 -22
- package/dist/api/client.d.ts +0 -12
- package/dist/api/client.js +0 -43
- package/dist/api/nes/nes.client.d.ts +0 -23
- package/dist/api/nes/nes.client.js +0 -107
- package/dist/api/queries/nes/sbom.d.ts +0 -3
- package/dist/api/queries/nes/sbom.js +0 -35
- 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 -53
- 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 -146
- package/dist/commands/report/purls.d.ts +0 -15
- package/dist/commands/report/purls.js +0 -84
- package/dist/commands/scan/sbom.d.ts +0 -21
- package/dist/commands/scan/sbom.js +0 -159
- 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 -14
- package/dist/service/eol/eol.svc.js +0 -49
- 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 -27
- package/dist/service/purls.svc.d.ts +0 -23
- package/dist/service/purls.svc.js +0 -99
- package/dist/ui/date.ui.d.ts +0 -1
- package/dist/ui/date.ui.js +0 -15
- package/dist/ui/eol.ui.d.ts +0 -15
- package/dist/ui/eol.ui.js +0 -134
- package/dist/ui/shared.ui.d.ts +0 -6
- package/dist/ui/shared.ui.js +0 -16
- /package/dist/hooks/{npm-update-notifier.d.ts → init/00_npm-update-notifier.d.ts} +0 -0
- /package/dist/hooks/{prerun.d.ts → prerun/prerun.d.ts} +0 -0
- /package/dist/hooks/{prerun.js → prerun/prerun.js} +0 -0
- /package/dist/service/{eol/sbom.worker.d.ts → sbom.worker.d.ts} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { track as _track, Identify, identify, init, setOptOut, Types } from '@amplitude/analytics-node';
|
|
3
|
+
import NodeMachineId from 'node-machine-id';
|
|
4
|
+
import { config } from "../config/constants.js";
|
|
5
|
+
const device_id = NodeMachineId.machineIdSync(true);
|
|
6
|
+
const started_at = new Date();
|
|
7
|
+
const session_id = started_at.getTime();
|
|
8
|
+
const defaultAnalyticsContext = {
|
|
9
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
10
|
+
os_platform: os.platform(),
|
|
11
|
+
os_release: os.release(),
|
|
12
|
+
cli_version: process.env.npm_package_version ?? 'unknown',
|
|
13
|
+
ci_provider: getCIProvider(),
|
|
14
|
+
app_used: getTerminal(),
|
|
15
|
+
started_at,
|
|
16
|
+
};
|
|
17
|
+
let analyticsContext = defaultAnalyticsContext;
|
|
18
|
+
export function initializeAnalytics() {
|
|
19
|
+
init('0', {
|
|
20
|
+
flushQueueSize: 2,
|
|
21
|
+
flushIntervalMillis: 250,
|
|
22
|
+
logLevel: Types.LogLevel.None,
|
|
23
|
+
serverUrl: config.analyticsUrl,
|
|
24
|
+
});
|
|
25
|
+
setOptOut(process.env.TRACKING_OPT_OUT === 'true');
|
|
26
|
+
identify(new Identify(), {
|
|
27
|
+
device_id,
|
|
28
|
+
platform: analyticsContext.os_platform,
|
|
29
|
+
os_name: getOSName(analyticsContext.os_platform ?? ''),
|
|
30
|
+
os_version: analyticsContext.os_release,
|
|
31
|
+
session_id,
|
|
32
|
+
app_version: analyticsContext.cli_version,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function track(event, getProperties) {
|
|
36
|
+
const localContext = getProperties?.(analyticsContext);
|
|
37
|
+
if (localContext) {
|
|
38
|
+
analyticsContext = { ...analyticsContext, ...localContext };
|
|
39
|
+
}
|
|
40
|
+
return _track(event, localContext, { device_id, session_id });
|
|
41
|
+
}
|
|
42
|
+
function getCIProvider(env = process.env) {
|
|
43
|
+
if (env.GITHUB_ACTIONS)
|
|
44
|
+
return 'github';
|
|
45
|
+
if (env.GITLAB_CI)
|
|
46
|
+
return 'gitlab';
|
|
47
|
+
if (env.CIRCLECI)
|
|
48
|
+
return 'circleci';
|
|
49
|
+
if (env.TF_BUILD)
|
|
50
|
+
return 'azure';
|
|
51
|
+
if (env.BITBUCKET_COMMIT || env.BITBUCKET_BUILD_NUMBER)
|
|
52
|
+
return 'bitbucket';
|
|
53
|
+
if (env.JENKINS_URL)
|
|
54
|
+
return 'jenkins';
|
|
55
|
+
if (env.BUILDKITE)
|
|
56
|
+
return 'buildkite';
|
|
57
|
+
if (env.TRAVIS)
|
|
58
|
+
return 'travis';
|
|
59
|
+
if (env.TEAMCITY_VERSION)
|
|
60
|
+
return 'teamcity';
|
|
61
|
+
if (env.CODEBUILD_BUILD_ID)
|
|
62
|
+
return 'codebuild';
|
|
63
|
+
if (env.CI)
|
|
64
|
+
return 'unknown_ci';
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function getTerminal(env = process.env) {
|
|
68
|
+
if (env.TERM_PROGRAM === 'vscode' || env.VSCODE_PID)
|
|
69
|
+
return 'vscode';
|
|
70
|
+
if (env.TERM_PROGRAM === 'iTerm.app' || env.ITERM_SESSION_ID)
|
|
71
|
+
return 'iterm';
|
|
72
|
+
if (env.TERM_PROGRAM === 'Apple_Terminal')
|
|
73
|
+
return 'apple_terminal';
|
|
74
|
+
if (env.TERM_PROGRAM === 'WarpTerminal' || env.WARP_IS_LOCAL_SHELL_SESSION)
|
|
75
|
+
return 'warp';
|
|
76
|
+
if (env.TERM_PROGRAM === 'ghostty' || env.GHOSTTY_RESOURCES_DIR || env.GHOSTTY_CONFIG_DIR)
|
|
77
|
+
return 'ghostty';
|
|
78
|
+
if (env.TERM_PROGRAM === 'WezTerm' || env.WEZTERM_EXECUTABLE || env.WEZTERM_PANE)
|
|
79
|
+
return 'wezterm';
|
|
80
|
+
if (env.TERM === 'alacritty' || env.ALACRITTY_LOG || env.ALACRITTY_SOCKET)
|
|
81
|
+
return 'alacritty';
|
|
82
|
+
if (env.TERM === 'xterm-kitty' || env.KITTY_WINDOW_ID)
|
|
83
|
+
return 'kitty';
|
|
84
|
+
if (env.WT_SESSION || env.WT_PROFILE_ID)
|
|
85
|
+
return 'windows_terminal';
|
|
86
|
+
if (env.ConEmuPID || env.ConEmuDir || env.CONEMU_BUILD)
|
|
87
|
+
return 'conemu';
|
|
88
|
+
if (env.TERM_PROGRAM === 'mintty' || env.MINTTY_SHORTCUT)
|
|
89
|
+
return 'mintty';
|
|
90
|
+
if (env.TILIX_ID)
|
|
91
|
+
return 'tilix';
|
|
92
|
+
if (env.GNOME_TERMINAL_SCREEN || env.GNOME_TERMINAL_SERVICE || env.VTE_VERSION)
|
|
93
|
+
return 'gnome';
|
|
94
|
+
if (env.KONSOLE_VERSION)
|
|
95
|
+
return 'konsole';
|
|
96
|
+
if (env.TERM_PROGRAM === 'Hyper')
|
|
97
|
+
return 'hyper';
|
|
98
|
+
return 'unknown_terminal';
|
|
99
|
+
}
|
|
100
|
+
function getOSName(platform) {
|
|
101
|
+
if (platform === 'darwin')
|
|
102
|
+
return 'macOS';
|
|
103
|
+
if (platform === 'win32')
|
|
104
|
+
return 'Windows';
|
|
105
|
+
if (platform === 'linux')
|
|
106
|
+
return 'Linux';
|
|
107
|
+
if (platform === 'android')
|
|
108
|
+
return 'Android';
|
|
109
|
+
if (platform === 'ios')
|
|
110
|
+
return 'iOS';
|
|
111
|
+
return platform;
|
|
112
|
+
}
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface SbomDependency {
|
|
3
|
-
ref: string;
|
|
4
|
-
dependsOn: string[];
|
|
5
|
-
}
|
|
6
|
-
export interface SbomEntry {
|
|
7
|
-
group: string;
|
|
8
|
-
name: string;
|
|
9
|
-
purl: string;
|
|
10
|
-
version: string;
|
|
11
|
-
}
|
|
12
|
-
export interface Sbom {
|
|
13
|
-
components: SbomEntry[];
|
|
14
|
-
dependencies: SbomDependency[];
|
|
15
|
-
}
|
|
1
|
+
import type { CdxBom } from '@herodevs/eol-shared';
|
|
16
2
|
export declare const SBOM_DEFAULT__OPTIONS: {
|
|
17
3
|
$0: string;
|
|
18
4
|
_: never[];
|
|
@@ -44,6 +30,7 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
44
30
|
o: string;
|
|
45
31
|
output: string;
|
|
46
32
|
outputFormat: string;
|
|
33
|
+
author: string[];
|
|
47
34
|
profile: string;
|
|
48
35
|
project: undefined;
|
|
49
36
|
'project-version': string;
|
|
@@ -61,6 +48,11 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
61
48
|
skipDtTlsCheck: boolean;
|
|
62
49
|
'spec-version': number;
|
|
63
50
|
specVersion: number;
|
|
51
|
+
tools: {
|
|
52
|
+
name: string;
|
|
53
|
+
publisher: string;
|
|
54
|
+
version: string;
|
|
55
|
+
}[];
|
|
64
56
|
'usages-slices-file': string;
|
|
65
57
|
usagesSlicesFile: string;
|
|
66
58
|
validate: boolean;
|
|
@@ -69,4 +61,4 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
69
61
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
70
62
|
* `directory`, and returns the `bomJson` property.
|
|
71
63
|
*/
|
|
72
|
-
export declare function
|
|
64
|
+
export declare function createSbom(directory: string): Promise<CdxBom>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
-
import { debugLogger } from "
|
|
2
|
+
import { debugLogger } from "./log.svc.js";
|
|
3
|
+
const author = process.env.npm_package_author ?? 'HeroDevs, Inc.';
|
|
3
4
|
export const SBOM_DEFAULT__OPTIONS = {
|
|
4
5
|
$0: 'cdxgen',
|
|
5
6
|
_: [],
|
|
@@ -23,8 +24,8 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
23
24
|
includeFormulation: false,
|
|
24
25
|
'no-install-deps': true,
|
|
25
26
|
noInstallDeps: true,
|
|
26
|
-
'min-confidence':
|
|
27
|
-
minConfidence:
|
|
27
|
+
'min-confidence': 1,
|
|
28
|
+
minConfidence: 1,
|
|
28
29
|
multiProject: true,
|
|
29
30
|
'no-banner': false,
|
|
30
31
|
noBabel: false,
|
|
@@ -32,7 +33,7 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
32
33
|
o: 'bom.json',
|
|
33
34
|
output: 'bom.json',
|
|
34
35
|
outputFormat: 'json', // or "xml"
|
|
35
|
-
|
|
36
|
+
author: [author],
|
|
36
37
|
profile: 'generic',
|
|
37
38
|
project: undefined,
|
|
38
39
|
'project-version': '',
|
|
@@ -50,6 +51,13 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
50
51
|
skipDtTlsCheck: true,
|
|
51
52
|
'spec-version': 1.6,
|
|
52
53
|
specVersion: 1.6,
|
|
54
|
+
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: '@herodevs/cli',
|
|
57
|
+
publisher: author,
|
|
58
|
+
version: process.env.npm_package_version ?? 'unknown',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
53
61
|
'usages-slices-file': 'usages.slices.json',
|
|
54
62
|
usagesSlicesFile: 'usages.slices.json',
|
|
55
63
|
validate: true,
|
|
@@ -58,8 +66,10 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
58
66
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
59
67
|
* `directory`, and returns the `bomJson` property.
|
|
60
68
|
*/
|
|
61
|
-
export async function
|
|
62
|
-
const sbom = await createBom(directory,
|
|
69
|
+
export async function createSbom(directory) {
|
|
70
|
+
const sbom = await createBom(directory, SBOM_DEFAULT__OPTIONS);
|
|
71
|
+
if (!sbom)
|
|
72
|
+
throw new Error('SBOM not generated');
|
|
63
73
|
debugLogger('Successfully generated SBOM');
|
|
64
|
-
return sbom
|
|
74
|
+
return sbom.bomJson;
|
|
65
75
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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[];
|
|
23
|
+
/**
|
|
24
|
+
* Formats data privacy information link for console display
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatDataPrivacyLink(): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Formats the report save hint for console display when the web report URL is hidden
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatReportSaveHint(): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
const SEPARATOR_WIDTH = 40;
|
|
11
|
+
/**
|
|
12
|
+
* Formats status row text with appropriate color and icon
|
|
13
|
+
*/
|
|
14
|
+
export const getStatusRowText = {
|
|
15
|
+
EOL: (text) => ux.colorize(STATUS_COLORS.EOL, `✗ ${text}`),
|
|
16
|
+
UNKNOWN: (text) => ux.colorize(STATUS_COLORS.UNKNOWN, `• ${text}`),
|
|
17
|
+
OK: (text) => ux.colorize(STATUS_COLORS.OK, `✔ ${text}`),
|
|
18
|
+
EOL_UPCOMING: (text) => ux.colorize(STATUS_COLORS.EOL_UPCOMING, `! ${text}`),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Counts components by their status, including NES remediation availability
|
|
22
|
+
*/
|
|
23
|
+
export function countComponentsByStatus(report) {
|
|
24
|
+
const grouped = {
|
|
25
|
+
UNKNOWN: 0,
|
|
26
|
+
OK: 0,
|
|
27
|
+
EOL_UPCOMING: 0,
|
|
28
|
+
EOL: 0,
|
|
29
|
+
NES_AVAILABLE: 0,
|
|
30
|
+
ECOSYSTEMS: [],
|
|
31
|
+
TOTAL: report.components.length,
|
|
32
|
+
};
|
|
33
|
+
const ecosystems = new Set();
|
|
34
|
+
for (const component of report.components) {
|
|
35
|
+
const status = deriveComponentStatus(component.metadata);
|
|
36
|
+
grouped[status]++;
|
|
37
|
+
if (component.nesRemediation?.remediations?.length) {
|
|
38
|
+
grouped.NES_AVAILABLE++;
|
|
39
|
+
}
|
|
40
|
+
const ecosystem = component.purl.match(/^pkg:([^/]+)\//)?.[1];
|
|
41
|
+
if (ecosystem) {
|
|
42
|
+
ecosystems.add(ecosystem);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
grouped.ECOSYSTEMS = Array.from(ecosystems);
|
|
46
|
+
return grouped;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Formats scan results for console display
|
|
50
|
+
*/
|
|
51
|
+
export function formatScanResults(report) {
|
|
52
|
+
const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(report);
|
|
53
|
+
if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) {
|
|
54
|
+
return [ux.colorize('yellow', 'No components found in scan.')];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
ux.colorize('bold', 'Scan results:'),
|
|
58
|
+
ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)),
|
|
59
|
+
ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`),
|
|
60
|
+
getStatusRowText.EOL(`${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`),
|
|
61
|
+
getStatusRowText.EOL_UPCOMING(`${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`),
|
|
62
|
+
getStatusRowText.OK(`${OK.toLocaleString().padEnd(5)} Not End-of-Life (EOL)`),
|
|
63
|
+
getStatusRowText.UNKNOWN(`${UNKNOWN.toLocaleString().padEnd(5)} Unknown EOL Status`),
|
|
64
|
+
getStatusRowText.UNKNOWN(`${NES_AVAILABLE.toLocaleString().padEnd(5)} HeroDevs NES Remediation${NES_AVAILABLE !== 1 ? 's' : ''} Available`),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Formats web report URL for console display
|
|
69
|
+
*/
|
|
70
|
+
export function formatWebReportUrl(id, reportCardUrl) {
|
|
71
|
+
const url = ux.colorize('blue', terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }));
|
|
72
|
+
return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), `🌐 View your full EOL report at: ${url}\n`];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Formats data privacy information link for console display
|
|
76
|
+
*/
|
|
77
|
+
export function formatDataPrivacyLink() {
|
|
78
|
+
const privacyUrl = 'https://docs.herodevs.com/eol-ds/data-privacy-and-security';
|
|
79
|
+
const link = ux.colorize('blue', terminalLink('Learn more about data privacy', privacyUrl, { fallback: (text, url) => `${text}: ${url}` }));
|
|
80
|
+
return [`🔒 ${link}\n`];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Formats the report save hint for console display when the web report URL is hidden
|
|
84
|
+
*/
|
|
85
|
+
export function formatReportSaveHint() {
|
|
86
|
+
return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), 'To save your detailed JSON report, use the --save flag'];
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 and converts it to CycloneDX format
|
|
7
|
+
* Supports both SPDX 2.3 and CycloneDX formats
|
|
8
|
+
*/
|
|
9
|
+
export declare function readSbomFromFile(filePath: string): CdxBom;
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a directory path exists and is actually a directory
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateDirectory(dirPath: string): void;
|
|
14
|
+
type SaveArtifactRequest = {
|
|
15
|
+
kind: 'sbom';
|
|
16
|
+
payload: CdxBom;
|
|
17
|
+
outputPath?: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'sbomTrimmed';
|
|
20
|
+
payload: CdxBom;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'report';
|
|
23
|
+
payload: EolReport;
|
|
24
|
+
outputPath?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Saves an SBOM, trimmed SBOM, or report to disk using the correct default filename.
|
|
28
|
+
*/
|
|
29
|
+
export declare function saveArtifactToFile(dir: string, request: SaveArtifactRequest): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path, { join, resolve } from 'node:path';
|
|
3
|
+
import { isCdxBom, isSpdxBom, spdxToCdxBom } from '@herodevs/eol-shared';
|
|
4
|
+
import { filenamePrefix } from "../config/constants.js";
|
|
5
|
+
import { getErrorMessage } from "./log.svc.js";
|
|
6
|
+
/**
|
|
7
|
+
* Computes an absolute output path using either a provided path or the base directory and default name.
|
|
8
|
+
*/
|
|
9
|
+
function resolveOutputPath(baseDir, defaultFilename, customPath) {
|
|
10
|
+
const defaultOutput = resolve(join(baseDir, defaultFilename));
|
|
11
|
+
if (!customPath) {
|
|
12
|
+
return { fileName: defaultFilename, fullPath: defaultOutput };
|
|
13
|
+
}
|
|
14
|
+
const resolvedCustomPath = resolve(customPath);
|
|
15
|
+
let targetPath = resolvedCustomPath;
|
|
16
|
+
const hasTrailingSeparator = /[\\/]$/.test(customPath);
|
|
17
|
+
const customIsDirectory = fs.existsSync(resolvedCustomPath) && fs.statSync(resolvedCustomPath).isDirectory();
|
|
18
|
+
if (hasTrailingSeparator || customIsDirectory) {
|
|
19
|
+
targetPath = join(resolvedCustomPath, defaultFilename);
|
|
20
|
+
}
|
|
21
|
+
return { fileName: path.basename(targetPath), fullPath: targetPath };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Ensures the output directory for a given path exists, is a directory, and is writable.
|
|
25
|
+
*/
|
|
26
|
+
function ensureOutputDirectory(fullPath, fileName) {
|
|
27
|
+
const targetDir = path.dirname(fullPath);
|
|
28
|
+
if (!fs.existsSync(targetDir)) {
|
|
29
|
+
throw new Error(`Unable to save ${fileName}`);
|
|
30
|
+
}
|
|
31
|
+
const stats = fs.statSync(targetDir);
|
|
32
|
+
if (!stats.isDirectory()) {
|
|
33
|
+
throw new Error(`Unable to save ${fileName}`);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
fs.accessSync(targetDir, fs.constants.W_OK);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`Unable to save ${fileName}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Writes JSON to disk after validating directory constraints and formats the payload for readability.
|
|
44
|
+
*/
|
|
45
|
+
function writeJsonFile(fullPath, fileName, payload, failureLabel) {
|
|
46
|
+
ensureOutputDirectory(fullPath, fileName);
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(fullPath, JSON.stringify(payload, null, 2));
|
|
49
|
+
return fullPath;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const fileError = error;
|
|
53
|
+
switch (fileError.code) {
|
|
54
|
+
case 'EACCES':
|
|
55
|
+
throw new Error(`Permission denied. Unable to save ${fileName}`);
|
|
56
|
+
case 'ENOSPC':
|
|
57
|
+
throw new Error(`No space left on device. Unable to save ${fileName}`);
|
|
58
|
+
case 'ENOENT':
|
|
59
|
+
case 'ENOTDIR':
|
|
60
|
+
throw new Error(`Unable to save ${fileName}`);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Failed to save ${failureLabel}: ${getErrorMessage(error)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads an SBOM from a file path and converts it to CycloneDX format
|
|
67
|
+
* Supports both SPDX 2.3 and CycloneDX formats
|
|
68
|
+
*/
|
|
69
|
+
export function readSbomFromFile(filePath) {
|
|
70
|
+
const file = resolve(filePath);
|
|
71
|
+
if (!fs.existsSync(file)) {
|
|
72
|
+
throw new Error(`SBOM file not found: ${file}`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const fileContent = fs.readFileSync(file, 'utf8');
|
|
76
|
+
const jsonContent = JSON.parse(fileContent);
|
|
77
|
+
if (isSpdxBom(jsonContent)) {
|
|
78
|
+
return spdxToCdxBom(jsonContent);
|
|
79
|
+
}
|
|
80
|
+
if (isCdxBom(jsonContent)) {
|
|
81
|
+
return jsonContent;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Invalid SBOM file format. Expected SPDX 2.3 or CycloneDX format.`);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validates that a directory path exists and is actually a directory
|
|
91
|
+
*/
|
|
92
|
+
export function validateDirectory(dirPath) {
|
|
93
|
+
const dir = resolve(dirPath);
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
96
|
+
}
|
|
97
|
+
const stats = fs.statSync(dir);
|
|
98
|
+
if (!stats.isDirectory()) {
|
|
99
|
+
throw new Error(`Path is not a directory: ${dir}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const artifactFilenames = {
|
|
103
|
+
sbom: `${filenamePrefix}.sbom.json`,
|
|
104
|
+
sbomTrimmed: `${filenamePrefix}.sbom-trimmed.json`,
|
|
105
|
+
report: `${filenamePrefix}.report.json`,
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Saves an SBOM, trimmed SBOM, or report to disk using the correct default filename.
|
|
109
|
+
*/
|
|
110
|
+
export function saveArtifactToFile(dir, request) {
|
|
111
|
+
const defaultFilename = artifactFilenames[request.kind];
|
|
112
|
+
const customOutputPath = 'outputPath' in request ? request.outputPath : undefined;
|
|
113
|
+
const { fileName, fullPath } = resolveOutputPath(dir, defaultFilename, customOutputPath);
|
|
114
|
+
return writeJsonFile(fullPath, fileName, request.payload, fileName);
|
|
115
|
+
}
|
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,6 +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 "../config/constants.js";
|
|
4
5
|
import { SBOM_DEFAULT__OPTIONS } from "./cdx.svc.js";
|
|
5
6
|
process.on('uncaughtException', (err) => {
|
|
6
7
|
console.error('Uncaught exception:', err.message);
|
|
@@ -15,7 +16,7 @@ try {
|
|
|
15
16
|
const options = JSON.parse(process.argv[2]);
|
|
16
17
|
const { path, opts } = options;
|
|
17
18
|
const { bomJson } = await createBom(path, { ...SBOM_DEFAULT__OPTIONS, ...opts });
|
|
18
|
-
const outputPath = join(path,
|
|
19
|
+
const outputPath = join(path, `${filenamePrefix}.sbom.json`);
|
|
19
20
|
writeFileSync(outputPath, JSON.stringify(bomJson, null, 2));
|
|
20
21
|
process.exit(0);
|
|
21
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stripTypename<T>(obj: T): T;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function stripTypename(obj) {
|
|
2
|
+
if (Array.isArray(obj)) {
|
|
3
|
+
return obj.map(stripTypename);
|
|
4
|
+
}
|
|
5
|
+
if (obj !== null && typeof obj === 'object') {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const key of Object.keys(obj)) {
|
|
8
|
+
if (key === '__typename')
|
|
9
|
+
continue;
|
|
10
|
+
result[key] = stripTypename(obj[key]);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
return obj;
|
|
15
|
+
}
|
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.11",
|
|
4
4
|
"author": "HeroDevs, Inc",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "./bin/run.js"
|
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
"ci": "biome ci",
|
|
18
18
|
"ci:fix": "biome check --write",
|
|
19
19
|
"clean": "shx rm -rf dist && npm run clean:files && shx rm -rf node_modules",
|
|
20
|
-
"clean:files": "shx rm -f
|
|
20
|
+
"clean:files": "shx rm -f herodevs.**.csv herodevs.**.json herodevs.**.txt",
|
|
21
21
|
"dev": "npm run build && ./bin/dev.js",
|
|
22
22
|
"dev:debug": "npm run build && DEBUG=oclif:* ./bin/dev.js",
|
|
23
23
|
"format": "biome format --write",
|
|
24
24
|
"lint": "biome lint --write",
|
|
25
25
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
26
|
-
"
|
|
26
|
+
"prepare": "shx test -d dist || npm run build",
|
|
27
|
+
"prepack": "oclif manifest",
|
|
27
28
|
"pretest": "npm run lint && npm run typecheck",
|
|
28
|
-
"readme": "npm run ci:fix && npm run build &&
|
|
29
|
-
"test": "globstar -- node --import tsx --test \"test/**/*.test.ts\"",
|
|
29
|
+
"readme": "npm run ci:fix && npm run build && oclif readme",
|
|
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",
|
|
34
|
+
"postversion": "node scripts/update-install-script-version.js && git add README.md"
|
|
32
35
|
},
|
|
33
36
|
"keywords": [
|
|
34
37
|
"herodevs",
|
|
@@ -36,30 +39,34 @@
|
|
|
36
39
|
"herodevs cli"
|
|
37
40
|
],
|
|
38
41
|
"dependencies": {
|
|
42
|
+
"@amplitude/analytics-node": "^1.5.14",
|
|
39
43
|
"@apollo/client": "^3.13.8",
|
|
40
|
-
"@cyclonedx/cdxgen": "
|
|
41
|
-
"@
|
|
42
|
-
"@oclif/
|
|
43
|
-
"@oclif/plugin-
|
|
44
|
-
"@oclif/
|
|
44
|
+
"@cyclonedx/cdxgen": "~11.4.4",
|
|
45
|
+
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.11",
|
|
46
|
+
"@oclif/core": "^4.5.3",
|
|
47
|
+
"@oclif/plugin-help": "^6.2.32",
|
|
48
|
+
"@oclif/plugin-update": "^4.7.8",
|
|
45
49
|
"graphql": "^16.11.0",
|
|
50
|
+
"node-machine-id": "^1.1.12",
|
|
51
|
+
"ora": "^8.2.0",
|
|
46
52
|
"packageurl-js": "^2.0.1",
|
|
53
|
+
"terminal-link": "^5.0.0",
|
|
47
54
|
"update-notifier": "^7.3.1"
|
|
48
55
|
},
|
|
49
56
|
"devDependencies": {
|
|
50
|
-
"@biomejs/biome": "^
|
|
51
|
-
"@oclif/test": "^4",
|
|
52
|
-
"@types/inquirer": "^9.0.
|
|
53
|
-
"@types/node": "^
|
|
57
|
+
"@biomejs/biome": "^2.2.2",
|
|
58
|
+
"@oclif/test": "^4.1.13",
|
|
59
|
+
"@types/inquirer": "^9.0.9",
|
|
60
|
+
"@types/node": "^24.7.0",
|
|
54
61
|
"@types/sinon": "^17.0.4",
|
|
55
62
|
"@types/update-notifier": "^6.0.8",
|
|
56
63
|
"globstar": "^1.0.0",
|
|
57
|
-
"oclif": "^4",
|
|
64
|
+
"oclif": "^4.22.29",
|
|
58
65
|
"shx": "^0.4.0",
|
|
59
|
-
"sinon": "^
|
|
60
|
-
"ts-node": "^10",
|
|
61
|
-
"tsx": "^4.
|
|
62
|
-
"typescript": "^5.
|
|
66
|
+
"sinon": "^21.0.0",
|
|
67
|
+
"ts-node": "^10.9.2",
|
|
68
|
+
"tsx": "^4.20.5",
|
|
69
|
+
"typescript": "^5.9.3"
|
|
63
70
|
},
|
|
64
71
|
"engines": {
|
|
65
72
|
"node": ">=20.0.0"
|
|
@@ -81,8 +88,12 @@
|
|
|
81
88
|
"@oclif/plugin-update"
|
|
82
89
|
],
|
|
83
90
|
"hooks": {
|
|
84
|
-
"init":
|
|
85
|
-
|
|
91
|
+
"init": [
|
|
92
|
+
"./dist/hooks/init/00_npm-update-notifier.js",
|
|
93
|
+
"./dist/hooks/init/01_initialize_amplitude.js"
|
|
94
|
+
],
|
|
95
|
+
"prerun": "./dist/hooks/prerun/prerun.js",
|
|
96
|
+
"finally": "./dist/hooks/finally/finally.js"
|
|
86
97
|
},
|
|
87
98
|
"topicSeparator": " ",
|
|
88
99
|
"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
|
-
}
|