@herodevs/cli 2.0.0-beta.8 → 2.0.0
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 +282 -39
- package/bin/main.js +2 -6
- package/dist/api/apollo.client.d.ts +3 -0
- package/dist/api/apollo.client.js +53 -0
- package/dist/api/ci-token.client.d.ts +27 -0
- package/dist/api/ci-token.client.js +95 -0
- package/dist/api/errors.d.ts +8 -0
- package/dist/api/errors.js +13 -0
- package/dist/api/gql-operations.d.ts +3 -0
- package/dist/api/gql-operations.js +36 -1
- package/dist/api/graphql-errors.d.ts +6 -0
- package/dist/api/graphql-errors.js +22 -0
- package/dist/api/nes.client.d.ts +1 -2
- package/dist/api/nes.client.js +40 -16
- package/dist/api/user-setup.client.d.ts +18 -0
- package/dist/api/user-setup.client.js +92 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.js +225 -0
- package/dist/commands/auth/logout.d.ts +5 -0
- package/dist/commands/auth/logout.js +27 -0
- package/dist/commands/auth/provision-ci-token.d.ts +5 -0
- package/dist/commands/auth/provision-ci-token.js +72 -0
- package/dist/commands/report/committers.d.ts +27 -0
- package/dist/commands/report/committers.js +215 -0
- package/dist/commands/scan/eol.d.ts +7 -0
- package/dist/commands/scan/eol.js +120 -32
- package/dist/commands/tracker/init.d.ts +14 -0
- package/dist/commands/tracker/init.js +84 -0
- package/dist/commands/tracker/run.d.ts +15 -0
- package/dist/commands/tracker/run.js +183 -0
- package/dist/config/constants.d.ts +14 -0
- package/dist/config/constants.js +15 -0
- package/dist/config/tracker.config.d.ts +16 -0
- package/dist/config/tracker.config.js +16 -0
- package/dist/hooks/finally/finally.js +13 -7
- package/dist/hooks/init/01_initialize_amplitude.js +20 -9
- package/dist/service/analytics.svc.d.ts +10 -4
- package/dist/service/analytics.svc.js +180 -18
- package/dist/service/auth-config.svc.d.ts +2 -0
- package/dist/service/auth-config.svc.js +8 -0
- package/dist/service/auth-refresh.svc.d.ts +8 -0
- package/dist/service/auth-refresh.svc.js +45 -0
- package/dist/service/auth-token.svc.d.ts +11 -0
- package/dist/service/auth-token.svc.js +62 -0
- package/dist/service/auth.svc.d.ts +27 -0
- package/dist/service/auth.svc.js +91 -0
- package/dist/service/cdx.svc.d.ts +9 -1
- package/dist/service/cdx.svc.js +17 -12
- package/dist/service/ci-auth.svc.d.ts +6 -0
- package/dist/service/ci-auth.svc.js +32 -0
- package/dist/service/ci-token.svc.d.ts +6 -0
- package/dist/service/ci-token.svc.js +44 -0
- package/dist/service/committers.svc.d.ts +58 -0
- package/dist/service/committers.svc.js +78 -0
- package/dist/service/display.svc.d.ts +8 -0
- package/dist/service/display.svc.js +17 -2
- package/dist/service/encrypted-store.svc.d.ts +5 -0
- package/dist/service/encrypted-store.svc.js +43 -0
- package/dist/service/error.svc.d.ts +8 -0
- package/dist/service/error.svc.js +28 -0
- package/dist/service/file.svc.d.ts +17 -7
- package/dist/service/file.svc.js +80 -36
- package/dist/service/jwt.svc.d.ts +1 -0
- package/dist/service/jwt.svc.js +19 -0
- package/dist/service/tracker.svc.d.ts +58 -0
- package/dist/service/tracker.svc.js +101 -0
- package/dist/types/auth.d.ts +9 -0
- package/dist/utils/open-in-browser.d.ts +1 -0
- package/dist/utils/open-in-browser.js +21 -0
- package/dist/utils/retry.d.ts +11 -0
- package/dist/utils/retry.js +29 -0
- package/dist/utils/strip-typename.d.ts +1 -0
- package/dist/utils/strip-typename.js +16 -0
- package/package.json +40 -22
- package/dist/service/sbom.worker.js +0 -26
- /package/dist/{service/sbom.worker.d.ts → types/auth.js} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface StoredTokens {
|
|
2
|
+
accessToken?: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function saveTokens(tokens: {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken?: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function getStoredTokens(): Promise<StoredTokens | undefined>;
|
|
10
|
+
export declare function clearStoredTokens(): Promise<void>;
|
|
11
|
+
export declare function isAccessTokenExpired(token: string | undefined): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createConfStore, decryptValue, encryptValue } from "./encrypted-store.svc.js";
|
|
2
|
+
import { decodeJwtPayload } from "./jwt.svc.js";
|
|
3
|
+
import { debugLogger } from "./log.svc.js";
|
|
4
|
+
const AUTH_TOKEN_SALT = 'hdcli-auth-token-v1';
|
|
5
|
+
const ACCESS_TOKEN_KEY = 'accessToken';
|
|
6
|
+
const REFRESH_TOKEN_KEY = 'refreshToken';
|
|
7
|
+
const TOKEN_SKEW_SECONDS = 30;
|
|
8
|
+
function getStore() {
|
|
9
|
+
return createConfStore('auth-token');
|
|
10
|
+
}
|
|
11
|
+
export async function saveTokens(tokens) {
|
|
12
|
+
const store = getStore();
|
|
13
|
+
store.set(ACCESS_TOKEN_KEY, encryptValue(tokens.accessToken, AUTH_TOKEN_SALT));
|
|
14
|
+
if (tokens.refreshToken) {
|
|
15
|
+
store.set(REFRESH_TOKEN_KEY, encryptValue(tokens.refreshToken, AUTH_TOKEN_SALT));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
store.delete(REFRESH_TOKEN_KEY);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function getStoredTokens() {
|
|
22
|
+
const store = getStore();
|
|
23
|
+
const encodedAccess = store.get(ACCESS_TOKEN_KEY);
|
|
24
|
+
const encodedRefresh = store.get(REFRESH_TOKEN_KEY);
|
|
25
|
+
let accessToken;
|
|
26
|
+
let refreshToken;
|
|
27
|
+
try {
|
|
28
|
+
if (encodedAccess && typeof encodedAccess === 'string') {
|
|
29
|
+
accessToken = decryptValue(encodedAccess, AUTH_TOKEN_SALT);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
debugLogger('Failed to decrypt access token: %O', error);
|
|
34
|
+
accessToken = undefined;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
if (encodedRefresh && typeof encodedRefresh === 'string') {
|
|
38
|
+
refreshToken = decryptValue(encodedRefresh, AUTH_TOKEN_SALT);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
debugLogger('Failed to decrypt refresh token: %O', error);
|
|
43
|
+
refreshToken = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!accessToken && !refreshToken) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return { accessToken, refreshToken };
|
|
49
|
+
}
|
|
50
|
+
export async function clearStoredTokens() {
|
|
51
|
+
const store = getStore();
|
|
52
|
+
store.delete(ACCESS_TOKEN_KEY);
|
|
53
|
+
store.delete(REFRESH_TOKEN_KEY);
|
|
54
|
+
}
|
|
55
|
+
export function isAccessTokenExpired(token) {
|
|
56
|
+
const payload = decodeJwtPayload(token);
|
|
57
|
+
if (!payload?.exp) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const now = Date.now() / 1000;
|
|
61
|
+
return now + TOKEN_SKEW_SECONDS >= payload.exp;
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TokenResponse } from '../types/auth.ts';
|
|
2
|
+
export type { CITokenErrorCode } from './ci-auth.svc.ts';
|
|
3
|
+
export { CITokenError } from './ci-auth.svc.ts';
|
|
4
|
+
export type AuthErrorCode = 'NOT_LOGGED_IN' | 'SESSION_EXPIRED';
|
|
5
|
+
export type TokenSource = 'oauth' | 'ci';
|
|
6
|
+
export type TokenProvider = (forceRefresh?: boolean) => Promise<string>;
|
|
7
|
+
export declare const AUTH_ERROR_MESSAGES: {
|
|
8
|
+
readonly UNAUTHENTICATED: "Please log in to perform this action. To authenticate, please run an \"auth login\" command.";
|
|
9
|
+
readonly SESSION_EXPIRED: "Your session has expired. To re-authenticate, please run an \"auth login\" command.";
|
|
10
|
+
readonly INVALID_TOKEN: "Your session has expired. To re-authenticate, please run an \"auth login\" command.";
|
|
11
|
+
readonly FORBIDDEN: "You do not have permission to perform this action.";
|
|
12
|
+
readonly NOT_LOGGED_IN_GENERIC: "You are not logged in. Please run an \"auth login\" command to authenticate.";
|
|
13
|
+
};
|
|
14
|
+
export declare function getTokenForScanWithSource(preferOAuth?: boolean, orgAccessToken?: string): Promise<{
|
|
15
|
+
token: string;
|
|
16
|
+
source: TokenSource;
|
|
17
|
+
}>;
|
|
18
|
+
export declare class AuthError extends Error {
|
|
19
|
+
readonly code: AuthErrorCode;
|
|
20
|
+
constructor(message: string, code: AuthErrorCode);
|
|
21
|
+
}
|
|
22
|
+
export declare function persistTokenResponse(token: TokenResponse): Promise<void>;
|
|
23
|
+
export declare function getAccessToken(): Promise<string | undefined>;
|
|
24
|
+
export declare function getTokenProvider(preferOAuth?: boolean, orgAccessToken?: string): TokenProvider;
|
|
25
|
+
export declare function requireAccessToken(): Promise<string>;
|
|
26
|
+
export declare function logoutLocally(): Promise<void>;
|
|
27
|
+
export declare const requireAccessTokenForScan: TokenProvider;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { refreshTokens } from "./auth-refresh.svc.js";
|
|
2
|
+
import { clearStoredTokens, getStoredTokens, isAccessTokenExpired, saveTokens } from "./auth-token.svc.js";
|
|
3
|
+
import { requireCIAccessToken } from "./ci-auth.svc.js";
|
|
4
|
+
import { getCIToken } from "./ci-token.svc.js";
|
|
5
|
+
import { debugLogger } from "./log.svc.js";
|
|
6
|
+
export { CITokenError } from "./ci-auth.svc.js";
|
|
7
|
+
export const AUTH_ERROR_MESSAGES = {
|
|
8
|
+
UNAUTHENTICATED: 'Please log in to perform this action. To authenticate, please run an "auth login" command.',
|
|
9
|
+
SESSION_EXPIRED: 'Your session has expired. To re-authenticate, please run an "auth login" command.',
|
|
10
|
+
INVALID_TOKEN: 'Your session has expired. To re-authenticate, please run an "auth login" command.',
|
|
11
|
+
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
12
|
+
NOT_LOGGED_IN_GENERIC: 'You are not logged in. Please run an "auth login" command to authenticate.',
|
|
13
|
+
};
|
|
14
|
+
export async function getTokenForScanWithSource(preferOAuth, orgAccessToken) {
|
|
15
|
+
if (orgAccessToken) {
|
|
16
|
+
return { token: orgAccessToken, source: 'ci' };
|
|
17
|
+
}
|
|
18
|
+
if (preferOAuth) {
|
|
19
|
+
const token = await requireAccessToken();
|
|
20
|
+
return { token, source: 'oauth' };
|
|
21
|
+
}
|
|
22
|
+
const tokens = await getStoredTokens();
|
|
23
|
+
if (tokens?.accessToken && !isAccessTokenExpired(tokens.accessToken)) {
|
|
24
|
+
return { token: tokens.accessToken, source: 'oauth' };
|
|
25
|
+
}
|
|
26
|
+
if (tokens?.refreshToken) {
|
|
27
|
+
try {
|
|
28
|
+
const newTokens = await refreshTokens(tokens.refreshToken);
|
|
29
|
+
await persistTokenResponse(newTokens);
|
|
30
|
+
return { token: newTokens.access_token, source: 'oauth' };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
debugLogger('Token refresh failed: %O', error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const ciToken = getCIToken();
|
|
37
|
+
if (ciToken) {
|
|
38
|
+
const accessToken = await requireCIAccessToken();
|
|
39
|
+
return { token: accessToken, source: 'ci' };
|
|
40
|
+
}
|
|
41
|
+
if (!tokens?.accessToken) {
|
|
42
|
+
throw new AuthError(AUTH_ERROR_MESSAGES.UNAUTHENTICATED, 'NOT_LOGGED_IN');
|
|
43
|
+
}
|
|
44
|
+
throw new AuthError(AUTH_ERROR_MESSAGES.SESSION_EXPIRED, 'SESSION_EXPIRED');
|
|
45
|
+
}
|
|
46
|
+
export class AuthError extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(message, code) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'AuthError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function persistTokenResponse(token) {
|
|
55
|
+
await saveTokens({
|
|
56
|
+
accessToken: token.access_token,
|
|
57
|
+
refreshToken: token.refresh_token,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function getAccessToken() {
|
|
61
|
+
const tokens = await getStoredTokens();
|
|
62
|
+
if (!tokens) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (tokens.accessToken && !isAccessTokenExpired(tokens.accessToken)) {
|
|
66
|
+
return tokens.accessToken;
|
|
67
|
+
}
|
|
68
|
+
if (!tokens.refreshToken) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const refreshed = await refreshTokens(tokens.refreshToken);
|
|
72
|
+
await persistTokenResponse(refreshed);
|
|
73
|
+
return refreshed.access_token;
|
|
74
|
+
}
|
|
75
|
+
export function getTokenProvider(preferOAuth, orgAccessToken) {
|
|
76
|
+
return async (_forceRefresh) => {
|
|
77
|
+
const { token } = await getTokenForScanWithSource(preferOAuth, orgAccessToken);
|
|
78
|
+
return token;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function requireAccessToken() {
|
|
82
|
+
const token = await getAccessToken();
|
|
83
|
+
if (!token) {
|
|
84
|
+
throw new Error(AUTH_ERROR_MESSAGES.NOT_LOGGED_IN_GENERIC);
|
|
85
|
+
}
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
export async function logoutLocally() {
|
|
89
|
+
await clearStoredTokens();
|
|
90
|
+
}
|
|
91
|
+
export const requireAccessTokenForScan = getTokenProvider();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
+
import { postProcess } from '@cyclonedx/cdxgen/stages/postgen/postgen';
|
|
1
3
|
import type { CdxBom } from '@herodevs/eol-shared';
|
|
2
4
|
export declare const SBOM_DEFAULT__OPTIONS: {
|
|
3
5
|
$0: string;
|
|
@@ -61,4 +63,10 @@ export declare const SBOM_DEFAULT__OPTIONS: {
|
|
|
61
63
|
* Lazy loads cdxgen (for ESM purposes), scans
|
|
62
64
|
* `directory`, and returns the `bomJson` property.
|
|
63
65
|
*/
|
|
64
|
-
|
|
66
|
+
type CreateSbomDependencies = {
|
|
67
|
+
createBom: typeof createBom;
|
|
68
|
+
postProcess: typeof postProcess;
|
|
69
|
+
};
|
|
70
|
+
export declare function createSbomFactory({ createBom: createBomDependency, postProcess: postProcessDependency, }?: Partial<CreateSbomDependencies>): (directory: string) => Promise<CdxBom>;
|
|
71
|
+
export declare const createSbom: (directory: string) => Promise<CdxBom>;
|
|
72
|
+
export {};
|
package/dist/service/cdx.svc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBom } from '@cyclonedx/cdxgen';
|
|
2
|
+
import { postProcess } from '@cyclonedx/cdxgen/stages/postgen/postgen';
|
|
2
3
|
import { debugLogger } from "./log.svc.js";
|
|
3
4
|
const author = process.env.npm_package_author ?? 'HeroDevs, Inc.';
|
|
4
5
|
export const SBOM_DEFAULT__OPTIONS = {
|
|
@@ -24,8 +25,8 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
24
25
|
includeFormulation: false,
|
|
25
26
|
'no-install-deps': true,
|
|
26
27
|
noInstallDeps: true,
|
|
27
|
-
'min-confidence': 1,
|
|
28
|
-
minConfidence: 1,
|
|
28
|
+
'min-confidence': 0.1,
|
|
29
|
+
minConfidence: 0.1,
|
|
29
30
|
multiProject: true,
|
|
30
31
|
'no-banner': false,
|
|
31
32
|
noBabel: false,
|
|
@@ -62,14 +63,18 @@ export const SBOM_DEFAULT__OPTIONS = {
|
|
|
62
63
|
usagesSlicesFile: 'usages.slices.json',
|
|
63
64
|
validate: true,
|
|
64
65
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
export function createSbomFactory({ createBom: createBomDependency = createBom, postProcess: postProcessDependency = postProcess, } = {}) {
|
|
67
|
+
return async function createSbom(directory) {
|
|
68
|
+
const sbom = await createBomDependency(directory, SBOM_DEFAULT__OPTIONS);
|
|
69
|
+
if (!sbom) {
|
|
70
|
+
throw new Error('SBOM not generated');
|
|
71
|
+
}
|
|
72
|
+
const postProcessedSbom = postProcessDependency(sbom, SBOM_DEFAULT__OPTIONS);
|
|
73
|
+
if (!postProcessedSbom) {
|
|
74
|
+
throw new Error('SBOM not generated');
|
|
75
|
+
}
|
|
76
|
+
debugLogger('Successfully generated SBOM');
|
|
77
|
+
return postProcessedSbom.bomJson;
|
|
78
|
+
};
|
|
75
79
|
}
|
|
80
|
+
export const createSbom = createSbomFactory();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type CITokenErrorCode = 'CI_TOKEN_INVALID' | 'CI_TOKEN_REFRESH_FAILED' | 'CI_ORG_ID_REQUIRED';
|
|
2
|
+
export declare class CITokenError extends Error {
|
|
3
|
+
readonly code: CITokenErrorCode;
|
|
4
|
+
constructor(message: string, code: CITokenErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export declare function requireCIAccessToken(): Promise<string>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { exchangeCITokenForAccess } from "../api/ci-token.client.js";
|
|
2
|
+
import { config } from "../config/constants.js";
|
|
3
|
+
import { getCIToken, saveCIToken } from "./ci-token.svc.js";
|
|
4
|
+
import { debugLogger } from "./log.svc.js";
|
|
5
|
+
const CITOKEN_ERROR_MESSAGE = "CI token is invalid or expired. To provision a new CI token, run 'hd auth provision-ci-token' (after logging in with 'hd auth login').";
|
|
6
|
+
export class CITokenError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'CITokenError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function requireCIAccessToken() {
|
|
15
|
+
const ciToken = getCIToken();
|
|
16
|
+
if (!ciToken) {
|
|
17
|
+
throw new CITokenError(CITOKEN_ERROR_MESSAGE, 'CI_TOKEN_INVALID');
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const result = await exchangeCITokenForAccess({
|
|
21
|
+
refreshToken: ciToken,
|
|
22
|
+
});
|
|
23
|
+
if (result.refreshToken && config.ciTokenFromEnv === undefined) {
|
|
24
|
+
saveCIToken(result.refreshToken);
|
|
25
|
+
}
|
|
26
|
+
return result.accessToken;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
debugLogger('CI token refresh failed: %O', error);
|
|
30
|
+
throw new CITokenError(CITOKEN_ERROR_MESSAGE, 'CI_TOKEN_REFRESH_FAILED');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function encryptToken(plaintext: string): string;
|
|
2
|
+
export declare function decryptToken(encoded: string): string;
|
|
3
|
+
export declare function getCITokenFromStorage(): string | undefined;
|
|
4
|
+
export declare function getCIToken(): string | undefined;
|
|
5
|
+
export declare function saveCIToken(token: string): void;
|
|
6
|
+
export declare function clearCIToken(): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { config } from "../config/constants.js";
|
|
2
|
+
import { createConfStore, decryptValue, encryptValue } from "./encrypted-store.svc.js";
|
|
3
|
+
import { debugLogger } from "./log.svc.js";
|
|
4
|
+
const CI_TOKEN_STORAGE_KEY = 'ciRefreshToken';
|
|
5
|
+
const CI_TOKEN_SALT = 'hdcli-ci-token-v1';
|
|
6
|
+
function getConfStore() {
|
|
7
|
+
return createConfStore('ci-token');
|
|
8
|
+
}
|
|
9
|
+
export function encryptToken(plaintext) {
|
|
10
|
+
return encryptValue(plaintext, CI_TOKEN_SALT);
|
|
11
|
+
}
|
|
12
|
+
export function decryptToken(encoded) {
|
|
13
|
+
return decryptValue(encoded, CI_TOKEN_SALT);
|
|
14
|
+
}
|
|
15
|
+
export function getCITokenFromStorage() {
|
|
16
|
+
const store = getConfStore();
|
|
17
|
+
const encoded = store.get(CI_TOKEN_STORAGE_KEY);
|
|
18
|
+
if (encoded === undefined || typeof encoded !== 'string') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return decryptToken(encoded);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
debugLogger('Failed to decrypt CI token: %O', error);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getCIToken() {
|
|
30
|
+
const fromEnv = config.ciTokenFromEnv;
|
|
31
|
+
if (fromEnv !== undefined) {
|
|
32
|
+
return fromEnv;
|
|
33
|
+
}
|
|
34
|
+
return getCITokenFromStorage();
|
|
35
|
+
}
|
|
36
|
+
export function saveCIToken(token) {
|
|
37
|
+
const store = getConfStore();
|
|
38
|
+
const encoded = encryptToken(token);
|
|
39
|
+
store.set(CI_TOKEN_STORAGE_KEY, encoded);
|
|
40
|
+
}
|
|
41
|
+
export function clearCIToken() {
|
|
42
|
+
const store = getConfStore();
|
|
43
|
+
store.delete(CI_TOKEN_STORAGE_KEY);
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type ReportFormat = 'txt' | 'csv' | 'json';
|
|
2
|
+
export type CommitEntry = {
|
|
3
|
+
commitHash: string;
|
|
4
|
+
author: string;
|
|
5
|
+
date: Date;
|
|
6
|
+
monthGroup: string;
|
|
7
|
+
};
|
|
8
|
+
export type CommitAuthorData = {
|
|
9
|
+
commits: CommitEntry[];
|
|
10
|
+
lastCommitOn: Date;
|
|
11
|
+
};
|
|
12
|
+
export type CommitMonthData = {
|
|
13
|
+
start: Date | string;
|
|
14
|
+
end: Date | string;
|
|
15
|
+
totalCommits: number;
|
|
16
|
+
committers: AuthorCommitCount;
|
|
17
|
+
};
|
|
18
|
+
export type AuthorCommitCount = {
|
|
19
|
+
[author: string]: number;
|
|
20
|
+
};
|
|
21
|
+
export type AuthorReportTableRow = {
|
|
22
|
+
index: number;
|
|
23
|
+
author: string;
|
|
24
|
+
commits: number;
|
|
25
|
+
lastCommitOn: string;
|
|
26
|
+
};
|
|
27
|
+
export type MonthlyReportTableRow = {
|
|
28
|
+
index: number;
|
|
29
|
+
month: number;
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
totalCommits: number;
|
|
33
|
+
};
|
|
34
|
+
export type MonthlyReportRow = {
|
|
35
|
+
month: string;
|
|
36
|
+
} & CommitMonthData;
|
|
37
|
+
export type AuthorReportRow = {
|
|
38
|
+
author: string;
|
|
39
|
+
} & CommitAuthorData;
|
|
40
|
+
export type CommittersReport = AuthorReportRow[] | MonthlyReportRow[];
|
|
41
|
+
/**
|
|
42
|
+
* Parses git log output into structured data
|
|
43
|
+
* @param output - Git log command output
|
|
44
|
+
* @returns Parsed commit entries
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseGitLogOutput(output: string): CommitEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Generates commits author report
|
|
49
|
+
* @param entries - commit entries from git log
|
|
50
|
+
* @returns Commits Author Report
|
|
51
|
+
*/
|
|
52
|
+
export declare function generateCommittersReport(entries: CommitEntry[]): AuthorReportRow[];
|
|
53
|
+
/**
|
|
54
|
+
* Generates commits monthly report
|
|
55
|
+
* @param entries - commit entries from git log
|
|
56
|
+
* @returns Monthly Report
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateMonthlyReport(entries: CommitEntry[]): MonthlyReportRow[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { endOfMonth, formatDate, parse } from 'date-fns';
|
|
2
|
+
import { DEFAULT_DATE_COMMIT_MONTH_FORMAT, DEFAULT_DATE_FORMAT } from '../config/constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses git log output into structured data
|
|
5
|
+
* @param output - Git log command output
|
|
6
|
+
* @returns Parsed commit entries
|
|
7
|
+
*/
|
|
8
|
+
export function parseGitLogOutput(output) {
|
|
9
|
+
return output
|
|
10
|
+
.split('\n')
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((line) => {
|
|
13
|
+
// Remove surrounding double quotes if present (e.g. "March|John Doe" → March|John Doe)
|
|
14
|
+
const [commitHash, author, date] = line.replace(/^"(.*)"$/, '$1').split('|');
|
|
15
|
+
return {
|
|
16
|
+
commitHash,
|
|
17
|
+
author,
|
|
18
|
+
date: parse(formatDate(new Date(date), DEFAULT_DATE_FORMAT), DEFAULT_DATE_FORMAT, new Date()),
|
|
19
|
+
monthGroup: formatDate(new Date(date), DEFAULT_DATE_COMMIT_MONTH_FORMAT),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates commits author report
|
|
25
|
+
* @param entries - commit entries from git log
|
|
26
|
+
* @returns Commits Author Report
|
|
27
|
+
*/
|
|
28
|
+
export function generateCommittersReport(entries) {
|
|
29
|
+
return Array.from(entries
|
|
30
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
31
|
+
.reduce((acc, curr, _index, array) => {
|
|
32
|
+
if (!acc.has(curr.author)) {
|
|
33
|
+
const byAuthor = array.filter((c) => c.author === curr.author);
|
|
34
|
+
acc.set(curr.author, {
|
|
35
|
+
commits: byAuthor,
|
|
36
|
+
lastCommitOn: byAuthor[0].date,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return acc;
|
|
40
|
+
}, new Map()))
|
|
41
|
+
.map(([key, value]) => ({
|
|
42
|
+
author: key,
|
|
43
|
+
commits: value.commits,
|
|
44
|
+
lastCommitOn: value.lastCommitOn,
|
|
45
|
+
}))
|
|
46
|
+
.sort((a, b) => b.commits.length - a.commits.length);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generates commits monthly report
|
|
50
|
+
* @param entries - commit entries from git log
|
|
51
|
+
* @returns Monthly Report
|
|
52
|
+
*/
|
|
53
|
+
export function generateMonthlyReport(entries) {
|
|
54
|
+
return Array.from(entries
|
|
55
|
+
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
|
|
56
|
+
.reduce((acc, curr, _index, array) => {
|
|
57
|
+
if (!acc.has(curr.monthGroup)) {
|
|
58
|
+
const monthlyCommits = array.filter((e) => e.monthGroup === curr.monthGroup);
|
|
59
|
+
acc.set(curr.monthGroup, {
|
|
60
|
+
start: formatDate(monthlyCommits[0].date, DEFAULT_DATE_FORMAT),
|
|
61
|
+
end: formatDate(endOfMonth(monthlyCommits[0].date), DEFAULT_DATE_FORMAT),
|
|
62
|
+
totalCommits: monthlyCommits.length,
|
|
63
|
+
committers: monthlyCommits.reduce((acc, curr) => {
|
|
64
|
+
if (!acc[curr.author]) {
|
|
65
|
+
acc[curr.author] = monthlyCommits.filter((c) => c.author === curr.author).length;
|
|
66
|
+
}
|
|
67
|
+
return acc;
|
|
68
|
+
}, {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return acc;
|
|
72
|
+
}, new Map()))
|
|
73
|
+
.map(([key, value]) => ({
|
|
74
|
+
month: key,
|
|
75
|
+
...value,
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => new Date(a.end).valueOf() - new Date(b.end).valueOf());
|
|
78
|
+
}
|
|
@@ -20,3 +20,11 @@ export declare function formatScanResults(report: EolReport): string[];
|
|
|
20
20
|
* Formats web report URL for console display
|
|
21
21
|
*/
|
|
22
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[];
|
|
@@ -7,6 +7,7 @@ const STATUS_COLORS = {
|
|
|
7
7
|
OK: 'green',
|
|
8
8
|
EOL_UPCOMING: 'yellow',
|
|
9
9
|
};
|
|
10
|
+
const SEPARATOR_WIDTH = 40;
|
|
10
11
|
/**
|
|
11
12
|
* Formats status row text with appropriate color and icon
|
|
12
13
|
*/
|
|
@@ -54,7 +55,7 @@ export function formatScanResults(report) {
|
|
|
54
55
|
}
|
|
55
56
|
return [
|
|
56
57
|
ux.colorize('bold', 'Scan results:'),
|
|
57
|
-
ux.colorize('bold', '-'.repeat(
|
|
58
|
+
ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)),
|
|
58
59
|
ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`),
|
|
59
60
|
getStatusRowText.EOL(`${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`),
|
|
60
61
|
getStatusRowText.EOL_UPCOMING(`${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`),
|
|
@@ -68,5 +69,19 @@ export function formatScanResults(report) {
|
|
|
68
69
|
*/
|
|
69
70
|
export function formatWebReportUrl(id, reportCardUrl) {
|
|
70
71
|
const url = ux.colorize('blue', terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }));
|
|
71
|
-
return [ux.colorize('bold', '-'.repeat(
|
|
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'];
|
|
72
87
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
export declare function getMachineKey(salt: string): Buffer;
|
|
3
|
+
export declare function encryptValue(plaintext: string, salt: string): string;
|
|
4
|
+
export declare function decryptValue(encoded: string, salt: string): string;
|
|
5
|
+
export declare function createConfStore(configName: string): Conf<Record<string, unknown>>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import Conf from 'conf';
|
|
5
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
6
|
+
const IV_LENGTH = 12;
|
|
7
|
+
const AUTH_TAG_LENGTH = 16;
|
|
8
|
+
export function getMachineKey(salt) {
|
|
9
|
+
const hostname = os.hostname();
|
|
10
|
+
const username = os.userInfo().username;
|
|
11
|
+
const raw = `${hostname}:${username}:${salt}`;
|
|
12
|
+
return crypto.createHash('sha256').update(raw, 'utf8').digest();
|
|
13
|
+
}
|
|
14
|
+
export function encryptValue(plaintext, salt) {
|
|
15
|
+
const key = getMachineKey(salt);
|
|
16
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
17
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
18
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
19
|
+
const authTag = cipher.getAuthTag();
|
|
20
|
+
const combined = Buffer.concat([iv, authTag, encrypted]);
|
|
21
|
+
return combined.toString('base64url');
|
|
22
|
+
}
|
|
23
|
+
export function decryptValue(encoded, salt) {
|
|
24
|
+
const key = getMachineKey(salt);
|
|
25
|
+
const combined = Buffer.from(encoded, 'base64url');
|
|
26
|
+
if (combined.length < IV_LENGTH + AUTH_TAG_LENGTH) {
|
|
27
|
+
throw new Error('Invalid encrypted token format');
|
|
28
|
+
}
|
|
29
|
+
const iv = combined.subarray(0, IV_LENGTH);
|
|
30
|
+
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
31
|
+
const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
32
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
33
|
+
decipher.setAuthTag(authTag);
|
|
34
|
+
return decipher.update(ciphertext).toString('utf8') + decipher.final('utf8');
|
|
35
|
+
}
|
|
36
|
+
export function createConfStore(configName) {
|
|
37
|
+
const cwd = path.join(os.homedir(), '.hdcli');
|
|
38
|
+
return new Conf({
|
|
39
|
+
projectName: 'hdcli',
|
|
40
|
+
cwd,
|
|
41
|
+
configName,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -3,18 +3,28 @@ export interface FileError extends Error {
|
|
|
3
3
|
code?: string;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Reads an SBOM from a file path
|
|
6
|
+
* Reads an SBOM from a file path and converts it to CycloneDX format
|
|
7
|
+
* Supports both SPDX 2.3 and CycloneDX formats
|
|
7
8
|
*/
|
|
8
9
|
export declare function readSbomFromFile(filePath: string): CdxBom;
|
|
9
10
|
/**
|
|
10
11
|
* Validates that a directory path exists and is actually a directory
|
|
11
12
|
*/
|
|
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
|
+
};
|
|
13
26
|
/**
|
|
14
|
-
* Saves an SBOM to
|
|
27
|
+
* Saves an SBOM, trimmed SBOM, or report to disk using the correct default filename.
|
|
15
28
|
*/
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
* Saves an EOL report to a file in the specified directory
|
|
19
|
-
*/
|
|
20
|
-
export declare function saveReportToFile(dir: string, report: EolReport): string;
|
|
29
|
+
export declare function saveArtifactToFile(dir: string, request: SaveArtifactRequest): string;
|
|
30
|
+
export {};
|