@herodevs/cli 2.0.0-beta.9 → 2.0.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 +253 -38
- 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 +113 -25
- 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 +38 -21
- package/dist/service/sbom.worker.js +0 -26
- /package/dist/{service/sbom.worker.d.ts → types/auth.js} +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { cwd } from 'node:process';
|
|
3
|
+
import { Command, Flags, ux } from '@oclif/core';
|
|
4
|
+
import { Presets, SingleBar } from 'cli-progress';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import terminalLink from 'terminal-link';
|
|
7
|
+
import { TRACKER_GIT_OUTPUT_FORMAT } from '../../config/constants.js';
|
|
8
|
+
import { getErrorMessage, isErrnoException } from '../../service/error.svc.js';
|
|
9
|
+
import { getConfiguration, getFileStats, getFilesFromCategory, getRootDir, INITIAL_FILES_STATS, saveResults, } from '../../service/tracker.svc.js';
|
|
10
|
+
export default class Run extends Command {
|
|
11
|
+
static description = 'Run the tracker';
|
|
12
|
+
static enableJsonFlag = false;
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> <%= command.id %>',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> -d tracker-configuration',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> -d tracker -f settings.json',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
configDir: Flags.string({
|
|
20
|
+
char: 'd',
|
|
21
|
+
description: 'Directory where the tracker configuration file resides',
|
|
22
|
+
default: 'hd-tracker',
|
|
23
|
+
}),
|
|
24
|
+
configFile: Flags.string({
|
|
25
|
+
char: 'f',
|
|
26
|
+
description: 'Filename for the tracker configuration file',
|
|
27
|
+
default: 'config.json',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async run() {
|
|
31
|
+
const { flags } = await this.parse(Run);
|
|
32
|
+
const { configDir, configFile } = flags;
|
|
33
|
+
try {
|
|
34
|
+
const rootDir = getRootDir(cwd());
|
|
35
|
+
const confSpinner = ora('Searching for configuration file').start();
|
|
36
|
+
const { categories, ignorePatterns, outputDir } = getConfiguration(rootDir, configDir, configFile);
|
|
37
|
+
confSpinner.text = `Configuration file ${configFile} found in ${rootDir}/${configDir}`;
|
|
38
|
+
const categoriesTotal = Object.keys(categories).length;
|
|
39
|
+
if (categoriesTotal > 0) {
|
|
40
|
+
confSpinner.stopAndPersist({
|
|
41
|
+
text: ux.colorize('green', `Found ${categoriesTotal} categor${categoriesTotal === 1 ? 'y' : 'ies'}`),
|
|
42
|
+
symbol: ux.colorize('green', `\u2714`),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
confSpinner.stopAndPersist({
|
|
47
|
+
text: ux.colorize('red', `No categories found, please check your configuration file`),
|
|
48
|
+
symbol: ux.colorize('red', `\u2716`),
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.log('');
|
|
53
|
+
const results = Object.entries(categories).reduce((acc, [name, category]) => {
|
|
54
|
+
const loadingFilesSpinner = ora(`[${ux.colorize('blueBright', name)}] Getting files`).start();
|
|
55
|
+
const fileProgress = new SingleBar({
|
|
56
|
+
format: `${ux.colorize('green', '{bar}')} | {value}/{total} | {name}`,
|
|
57
|
+
clearOnComplete: false,
|
|
58
|
+
fps: 100,
|
|
59
|
+
hideCursor: true,
|
|
60
|
+
}, Presets.shades_grey);
|
|
61
|
+
const fileTypes = new Set();
|
|
62
|
+
const categoryFilesWithError = [];
|
|
63
|
+
const files = getFilesFromCategory(category, {
|
|
64
|
+
rootDir,
|
|
65
|
+
ignorePatterns,
|
|
66
|
+
});
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
loadingFilesSpinner.stopAndPersist({
|
|
69
|
+
text: ux.colorize('yellow', `[${ux.colorize('yellowBright', name)}] Found 0 files`),
|
|
70
|
+
symbol: ux.colorize('yellowBright', `\u26A0`),
|
|
71
|
+
});
|
|
72
|
+
this.log(ux.colorize('yellow', `Please check your configuration [includes] property so it matches folders in your project directory`));
|
|
73
|
+
this.log('');
|
|
74
|
+
return acc;
|
|
75
|
+
}
|
|
76
|
+
loadingFilesSpinner.stopAndPersist({
|
|
77
|
+
text: ux.colorize('green', `[${ux.colorize('blueBright', name)}] Found ${files.length} files`),
|
|
78
|
+
symbol: ux.colorize('green', `\u2714`),
|
|
79
|
+
});
|
|
80
|
+
fileProgress.start(files.length, 1);
|
|
81
|
+
const fileResults = files.reduce((result, file, currentIndex, array) => {
|
|
82
|
+
const fileStats = getFileStats(file, {
|
|
83
|
+
rootDir,
|
|
84
|
+
});
|
|
85
|
+
if (currentIndex === array.length - 1) {
|
|
86
|
+
fileProgress.update({
|
|
87
|
+
name: ux.colorize('green', 'All files were processed successfully'),
|
|
88
|
+
});
|
|
89
|
+
fileProgress.stop();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
fileProgress.increment({
|
|
93
|
+
name: file,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if ('error' in fileStats) {
|
|
97
|
+
categoryFilesWithError.push(file);
|
|
98
|
+
fileProgress.increment();
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
fileTypes.add(fileStats.fileType);
|
|
103
|
+
return {
|
|
104
|
+
total: fileStats.total + result.total,
|
|
105
|
+
block: fileStats.block + result.block,
|
|
106
|
+
blockEmpty: fileStats.blockEmpty + result.blockEmpty,
|
|
107
|
+
comment: fileStats.comment + result.comment,
|
|
108
|
+
empty: fileStats.empty + result.empty,
|
|
109
|
+
mixed: fileStats.mixed + result.mixed,
|
|
110
|
+
single: fileStats.single + result.single,
|
|
111
|
+
source: fileStats.source + result.source,
|
|
112
|
+
todo: fileStats.todo + result.todo,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}, INITIAL_FILES_STATS);
|
|
116
|
+
this.log('');
|
|
117
|
+
acc.push({
|
|
118
|
+
name,
|
|
119
|
+
totals: fileResults,
|
|
120
|
+
errors: categoryFilesWithError,
|
|
121
|
+
fileTypes: Array.from(fileTypes),
|
|
122
|
+
});
|
|
123
|
+
return acc;
|
|
124
|
+
}, []);
|
|
125
|
+
this.log('');
|
|
126
|
+
const spinner = ora('Saving results').start();
|
|
127
|
+
const resultsLink = saveResults(results, rootDir, outputDir, this.fetchGitLastCommit(rootDir));
|
|
128
|
+
spinner.stopAndPersist({
|
|
129
|
+
text: ux.colorize('green', 'Tracker results saved!\n'),
|
|
130
|
+
symbol: ux.colorize('green', '\u2713'),
|
|
131
|
+
});
|
|
132
|
+
this.log(`${ux.colorize('blueBright', terminalLink(`Open Tracker Results`, `file://${resultsLink}`))}\n`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof Error) {
|
|
136
|
+
this.error(ux.colorize('red', err.message));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Fetches Git last commit
|
|
142
|
+
*/
|
|
143
|
+
fetchGitLastCommit(rootDir) {
|
|
144
|
+
const logParameters = ['log', `-1`, `--format=${TRACKER_GIT_OUTPUT_FORMAT}`, ...(rootDir ? ['--', rootDir] : [])];
|
|
145
|
+
const logProcess = spawnSync('git', logParameters, {
|
|
146
|
+
encoding: 'utf-8',
|
|
147
|
+
});
|
|
148
|
+
if (logProcess.error) {
|
|
149
|
+
if (isErrnoException(logProcess.error)) {
|
|
150
|
+
if (logProcess.error.code === 'ENOENT') {
|
|
151
|
+
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
|
|
152
|
+
}
|
|
153
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
154
|
+
}
|
|
155
|
+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
156
|
+
}
|
|
157
|
+
if (logProcess.status !== 0) {
|
|
158
|
+
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
|
|
159
|
+
}
|
|
160
|
+
if (!logProcess.stdout) {
|
|
161
|
+
return {
|
|
162
|
+
hash: '',
|
|
163
|
+
timestamp: '',
|
|
164
|
+
author: '',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return logProcess.stdout
|
|
168
|
+
.split('\n')
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.reduce((_acc, curr) => {
|
|
171
|
+
const [hash, author, timestamp] = curr.replace(/^"(.*)"$/, '$1').split('|');
|
|
172
|
+
return {
|
|
173
|
+
timestamp,
|
|
174
|
+
hash,
|
|
175
|
+
author,
|
|
176
|
+
};
|
|
177
|
+
}, {
|
|
178
|
+
hash: '',
|
|
179
|
+
timestamp: '',
|
|
180
|
+
author: '',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -4,6 +4,17 @@ export declare const GRAPHQL_PATH = "/graphql";
|
|
|
4
4
|
export declare const ANALYTICS_URL = "https://apps.herodevs.com/api/eol/track";
|
|
5
5
|
export declare const CONCURRENT_PAGE_REQUESTS = 3;
|
|
6
6
|
export declare const PAGE_SIZE = 500;
|
|
7
|
+
export declare const GIT_OUTPUT_FORMAT: string;
|
|
8
|
+
export declare const DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
|
9
|
+
export declare const DEFAULT_DATE_COMMIT_FORMAT = "MM/dd/yyyy, h:mm:ss a";
|
|
10
|
+
export declare const DEFAULT_DATE_COMMIT_MONTH_FORMAT = "MMMM yyyy";
|
|
11
|
+
export declare const DEFAULT_TRACKER_RUN_DATA_FILE = "data.json";
|
|
12
|
+
export declare const TRACKER_GIT_OUTPUT_FORMAT: string;
|
|
13
|
+
export declare const OAUTH_CALLBACK_ERROR_CODES: {
|
|
14
|
+
readonly ALREADY_LOGGED_IN: "already_logged_in";
|
|
15
|
+
readonly DIFFERENT_USER_AUTHENTICATED: "different_user_authenticated";
|
|
16
|
+
};
|
|
17
|
+
export type OAuthCallbackErrorCode = (typeof OAUTH_CALLBACK_ERROR_CODES)[keyof typeof OAUTH_CALLBACK_ERROR_CODES];
|
|
7
18
|
export declare const config: {
|
|
8
19
|
eolReportUrl: string;
|
|
9
20
|
graphqlHost: string;
|
|
@@ -11,5 +22,8 @@ export declare const config: {
|
|
|
11
22
|
analyticsUrl: string;
|
|
12
23
|
concurrentPageRequests: number;
|
|
13
24
|
pageSize: number;
|
|
25
|
+
ciTokenFromEnv: string | undefined;
|
|
14
26
|
};
|
|
15
27
|
export declare const filenamePrefix = "herodevs";
|
|
28
|
+
export declare const SCAN_ORIGIN_CLI = "CLI Scan";
|
|
29
|
+
export declare const SCAN_ORIGIN_AUTOMATED = "Automated Scan";
|
package/dist/config/constants.js
CHANGED
|
@@ -4,6 +4,18 @@ export const GRAPHQL_PATH = '/graphql';
|
|
|
4
4
|
export const ANALYTICS_URL = 'https://apps.herodevs.com/api/eol/track';
|
|
5
5
|
export const CONCURRENT_PAGE_REQUESTS = 3;
|
|
6
6
|
export const PAGE_SIZE = 500;
|
|
7
|
+
export const GIT_OUTPUT_FORMAT = `"${['%h', '%an', '%ad'].join('|')}"`;
|
|
8
|
+
// Committers Report - Date Constants
|
|
9
|
+
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
|
|
10
|
+
export const DEFAULT_DATE_COMMIT_FORMAT = 'MM/dd/yyyy, h:mm:ss a';
|
|
11
|
+
export const DEFAULT_DATE_COMMIT_MONTH_FORMAT = 'MMMM yyyy';
|
|
12
|
+
// Trackers - Constants
|
|
13
|
+
export const DEFAULT_TRACKER_RUN_DATA_FILE = 'data.json';
|
|
14
|
+
export const TRACKER_GIT_OUTPUT_FORMAT = `"${['%H', '%an', '%ad'].join('|')}"`;
|
|
15
|
+
export const OAUTH_CALLBACK_ERROR_CODES = {
|
|
16
|
+
ALREADY_LOGGED_IN: 'already_logged_in',
|
|
17
|
+
DIFFERENT_USER_AUTHENTICATED: 'different_user_authenticated',
|
|
18
|
+
};
|
|
7
19
|
let concurrentPageRequests = CONCURRENT_PAGE_REQUESTS;
|
|
8
20
|
const parsed = Number.parseInt(process.env.CONCURRENT_PAGE_REQUESTS ?? '0', 10);
|
|
9
21
|
if (parsed > 0) {
|
|
@@ -21,5 +33,8 @@ export const config = {
|
|
|
21
33
|
analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
|
|
22
34
|
concurrentPageRequests,
|
|
23
35
|
pageSize,
|
|
36
|
+
ciTokenFromEnv: process.env.HD_CI_CREDENTIAL?.trim() || undefined,
|
|
24
37
|
};
|
|
25
38
|
export const filenamePrefix = 'herodevs';
|
|
39
|
+
export const SCAN_ORIGIN_CLI = 'CLI Scan';
|
|
40
|
+
export const SCAN_ORIGIN_AUTOMATED = 'Automated Scan';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TrackerCategoryDefinition {
|
|
2
|
+
fileTypes: string[];
|
|
3
|
+
includes: string[];
|
|
4
|
+
excludes?: string[];
|
|
5
|
+
jsTsPairs?: 'js' | 'ts' | 'ignore';
|
|
6
|
+
}
|
|
7
|
+
export type TrackerConfig = {
|
|
8
|
+
categories: {
|
|
9
|
+
[key: string]: TrackerCategoryDefinition;
|
|
10
|
+
};
|
|
11
|
+
ignorePatterns?: string[];
|
|
12
|
+
outputDir: string;
|
|
13
|
+
configFile: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const TRACKER_ROOT_FILE = "package.json";
|
|
16
|
+
export declare const TRACKER_DEFAULT_CONFIG: TrackerConfig;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const TRACKER_ROOT_FILE = 'package.json';
|
|
2
|
+
export const TRACKER_DEFAULT_CONFIG = {
|
|
3
|
+
categories: {
|
|
4
|
+
legacy: {
|
|
5
|
+
fileTypes: ['js', 'ts', 'html', 'css', 'scss', 'less'],
|
|
6
|
+
includes: ['./legacy'],
|
|
7
|
+
},
|
|
8
|
+
modern: {
|
|
9
|
+
fileTypes: ['ts', 'html', 'css', 'scss', 'less'],
|
|
10
|
+
includes: ['./modern'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
ignorePatterns: ['**/node_modules/**'],
|
|
14
|
+
outputDir: 'hd-tracker',
|
|
15
|
+
configFile: 'config.json',
|
|
16
|
+
};
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import ora, {} from 'ora';
|
|
2
2
|
import { track } from "../../service/analytics.svc.js";
|
|
3
|
+
import { debugLogger, getErrorMessage } from "../../service/log.svc.js";
|
|
3
4
|
const hook = async (opts) => {
|
|
4
5
|
const isHelpOrVersionCmd = opts.argv.includes('--help') || opts.argv.includes('--version');
|
|
6
|
+
const hasError = Boolean(opts.error);
|
|
5
7
|
let spinner;
|
|
6
|
-
if (!isHelpOrVersionCmd) {
|
|
8
|
+
if (!isHelpOrVersionCmd && !hasError) {
|
|
7
9
|
spinner = ora().start('Cleaning up');
|
|
8
10
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
try {
|
|
12
|
+
await track('CLI Session Ended', (context) => ({
|
|
13
|
+
cli_version: context.cli_version,
|
|
14
|
+
ended_at: new Date(),
|
|
15
|
+
})).promise;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
debugLogger('Failed to track CLI session end: %s', getErrorMessage(error));
|
|
19
|
+
}
|
|
20
|
+
if (!isHelpOrVersionCmd && !hasError) {
|
|
15
21
|
spinner?.stop();
|
|
16
22
|
}
|
|
17
23
|
};
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { initializeAnalytics, track } from "../../service/analytics.svc.js";
|
|
3
|
+
import { debugLogger, getErrorMessage } from "../../service/log.svc.js";
|
|
3
4
|
const hook = async () => {
|
|
4
5
|
const args = parseArgs({ allowPositionals: true, strict: false });
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
try {
|
|
7
|
+
await initializeAnalytics();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
debugLogger('Failed to initialize analytics in init hook: %s', getErrorMessage(error));
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
track('CLI Command Submitted', (context) => ({
|
|
14
|
+
command: args.positionals.join(' ').trim(),
|
|
15
|
+
command_flags: Object.entries(args.values).flat().join(' '),
|
|
16
|
+
app_used: context.app_used,
|
|
17
|
+
ci_provider: context.ci_provider,
|
|
18
|
+
cli_version: context.cli_version,
|
|
19
|
+
started_at: context.started_at,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
debugLogger('Failed to track command submission: %s', getErrorMessage(error));
|
|
24
|
+
}
|
|
14
25
|
};
|
|
15
26
|
export default hook;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { Types } from '@amplitude/analytics-node';
|
|
2
1
|
interface AnalyticsContext {
|
|
3
2
|
locale?: string;
|
|
4
3
|
os_platform?: string;
|
|
5
4
|
os_release?: string;
|
|
6
5
|
started_at?: Date;
|
|
7
6
|
ended_at?: Date;
|
|
7
|
+
email?: string;
|
|
8
|
+
organization_name?: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
user_id?: string;
|
|
8
11
|
app_used?: string;
|
|
9
12
|
ci_provider?: string;
|
|
10
13
|
cli_version?: string;
|
|
11
14
|
command?: string;
|
|
12
15
|
command_flags?: string;
|
|
13
16
|
error?: string;
|
|
14
|
-
scan_location?: string;
|
|
15
17
|
eol_true_count?: number;
|
|
16
18
|
eol_unknown_count?: number;
|
|
17
19
|
nes_available_count?: number;
|
|
@@ -23,6 +25,10 @@ interface AnalyticsContext {
|
|
|
23
25
|
scan_failure_reason?: string;
|
|
24
26
|
web_report_link?: string;
|
|
25
27
|
}
|
|
26
|
-
export declare function initializeAnalytics(): void
|
|
27
|
-
export declare function
|
|
28
|
+
export declare function initializeAnalytics(): Promise<void>;
|
|
29
|
+
export declare function refreshIdentityFromStoredToken(): Promise<boolean>;
|
|
30
|
+
export declare function clearTrackedIdentity(): void;
|
|
31
|
+
export declare function track(event: string, getProperties?: (context: AnalyticsContext) => Partial<AnalyticsContext>): {
|
|
32
|
+
promise: Promise<void>;
|
|
33
|
+
};
|
|
28
34
|
export {};
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import os from 'node:os';
|
|
2
3
|
import { track as _track, Identify, identify, init, setOptOut, Types } from '@amplitude/analytics-node';
|
|
3
4
|
import NodeMachineId from 'node-machine-id';
|
|
4
5
|
import { config } from "../config/constants.js";
|
|
5
|
-
|
|
6
|
+
import { getStoredTokens } from "./auth-token.svc.js";
|
|
7
|
+
import { decodeJwtPayload } from "./jwt.svc.js";
|
|
8
|
+
import { debugLogger, getErrorMessage } from "./log.svc.js";
|
|
9
|
+
const SOURCE = 'cli';
|
|
10
|
+
const device_id = resolveDeviceId();
|
|
6
11
|
const started_at = new Date();
|
|
7
12
|
const session_id = started_at.getTime();
|
|
13
|
+
const IDENTITY_FIELDS = ['email', 'organization_name', 'role', 'user_id'];
|
|
14
|
+
const CONTEXT_IDENTITY_FIELDS = ['email', 'organization_name', 'role', 'user_id'];
|
|
8
15
|
const defaultAnalyticsContext = {
|
|
9
16
|
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
10
17
|
os_platform: os.platform(),
|
|
@@ -15,29 +22,184 @@ const defaultAnalyticsContext = {
|
|
|
15
22
|
started_at,
|
|
16
23
|
};
|
|
17
24
|
let analyticsContext = defaultAnalyticsContext;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
let identifiedUserId;
|
|
26
|
+
let lastIdentitySignature = '';
|
|
27
|
+
export async function initializeAnalytics() {
|
|
28
|
+
try {
|
|
29
|
+
await toSafeAnalyticsResult(init('0', {
|
|
30
|
+
flushQueueSize: 2,
|
|
31
|
+
flushIntervalMillis: 250,
|
|
32
|
+
logLevel: Types.LogLevel.None,
|
|
33
|
+
serverUrl: config.analyticsUrl,
|
|
34
|
+
}), 'init').promise;
|
|
35
|
+
void toSafeAnalyticsResult(setOptOut(process.env.TRACKING_OPT_OUT === 'true'), 'setOptOut').promise;
|
|
36
|
+
const identifiedFromToken = await refreshIdentityFromStoredToken();
|
|
37
|
+
if (!identifiedFromToken) {
|
|
38
|
+
void toSafeAnalyticsResult(identify(new Identify(), buildIdentifyEventOptions()), 'identify-anonymous').promise;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logAnalyticsError('initialize', error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function refreshIdentityFromStoredToken() {
|
|
46
|
+
try {
|
|
47
|
+
const tokens = await getStoredTokens();
|
|
48
|
+
const claims = resolveIdentityClaims(tokens);
|
|
49
|
+
if (!claims) {
|
|
50
|
+
clearTrackedIdentity();
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const entries = toIdentityEntries(claims);
|
|
54
|
+
const signature = buildIdentitySignature(entries);
|
|
55
|
+
if (signature === lastIdentitySignature) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
applyIdentityClaims(claims, signature);
|
|
59
|
+
emitIdentify(entries, claims.user_id);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logAnalyticsError('refreshIdentityFromStoredToken', error);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function clearTrackedIdentity() {
|
|
68
|
+
identifiedUserId = undefined;
|
|
69
|
+
lastIdentitySignature = '';
|
|
70
|
+
analyticsContext = clearIdentityFromContext(analyticsContext);
|
|
71
|
+
}
|
|
72
|
+
export function track(event, getProperties) {
|
|
73
|
+
try {
|
|
74
|
+
const localContext = getProperties?.(analyticsContext);
|
|
75
|
+
if (localContext) {
|
|
76
|
+
analyticsContext = { ...analyticsContext, ...localContext };
|
|
77
|
+
}
|
|
78
|
+
const eventProperties = { source: SOURCE, ...(localContext ?? {}) };
|
|
79
|
+
return toSafeAnalyticsResult(_track(event, eventProperties, buildEventOptions(identifiedUserId || analyticsContext.user_id)), `track:${event}`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logAnalyticsError(`track:${event}`, error);
|
|
83
|
+
return toSafeAnalyticsResult(undefined, `track:${event}:noop`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildEventOptions(userId) {
|
|
87
|
+
if (userId) {
|
|
88
|
+
return { device_id, session_id, user_id: userId };
|
|
89
|
+
}
|
|
90
|
+
return { device_id, session_id };
|
|
91
|
+
}
|
|
92
|
+
function buildIdentifyEventOptions(userId) {
|
|
93
|
+
return {
|
|
94
|
+
...buildEventOptions(userId),
|
|
28
95
|
platform: analyticsContext.os_platform,
|
|
29
96
|
os_name: getOSName(analyticsContext.os_platform ?? ''),
|
|
30
97
|
os_version: analyticsContext.os_release,
|
|
31
|
-
session_id,
|
|
32
98
|
app_version: analyticsContext.cli_version,
|
|
33
|
-
}
|
|
99
|
+
};
|
|
34
100
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
101
|
+
function normalizeClaim(value) {
|
|
102
|
+
if (typeof value !== 'string') {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
return value.trim();
|
|
106
|
+
}
|
|
107
|
+
function extractIdentityClaims(accessToken) {
|
|
108
|
+
const payload = decodeJwtPayload(accessToken);
|
|
109
|
+
if (!payload) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const identity = {
|
|
113
|
+
user_id: normalizeClaim(payload.sub) || undefined,
|
|
114
|
+
email: normalizeClaim(payload.email) || undefined,
|
|
115
|
+
organization_name: normalizeClaim(payload.company) || undefined,
|
|
116
|
+
role: normalizeClaim(payload.role) || undefined,
|
|
117
|
+
};
|
|
118
|
+
if (toIdentityEntries(identity).length === 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return identity;
|
|
122
|
+
}
|
|
123
|
+
function resolveIdentityClaims(tokens) {
|
|
124
|
+
const tokenCandidates = new Set([tokens?.accessToken, config.ciTokenFromEnv].map((token) => normalizeClaim(token)).filter(Boolean));
|
|
125
|
+
for (const tokenCandidate of tokenCandidates) {
|
|
126
|
+
const claims = extractIdentityClaims(tokenCandidate);
|
|
127
|
+
if (claims) {
|
|
128
|
+
return claims;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
function toIdentityEntries(identity) {
|
|
134
|
+
const entries = [];
|
|
135
|
+
for (const field of IDENTITY_FIELDS) {
|
|
136
|
+
const value = identity[field];
|
|
137
|
+
if (value) {
|
|
138
|
+
entries.push([field, value]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
function buildIdentitySignature(entries) {
|
|
144
|
+
return entries.map(([field, value]) => `${field}:${value}`).join('|');
|
|
145
|
+
}
|
|
146
|
+
function applyIdentityClaims(claims, signature) {
|
|
147
|
+
identifiedUserId = claims.user_id;
|
|
148
|
+
lastIdentitySignature = signature;
|
|
149
|
+
analyticsContext = { ...analyticsContext, ...claims };
|
|
150
|
+
}
|
|
151
|
+
function emitIdentify(entries, userId) {
|
|
152
|
+
const amplitudeIdentify = new Identify();
|
|
153
|
+
for (const [field, value] of entries) {
|
|
154
|
+
amplitudeIdentify.set(field, value);
|
|
155
|
+
}
|
|
156
|
+
const eventOptions = buildIdentifyEventOptions(userId);
|
|
157
|
+
void toSafeAnalyticsResult(identify(amplitudeIdentify, eventOptions), 'identify').promise;
|
|
158
|
+
void toSafeAnalyticsResult(_track('Identify Call', { source: SOURCE }, eventOptions), 'track:Identify Call').promise;
|
|
159
|
+
}
|
|
160
|
+
function resolveDeviceId() {
|
|
161
|
+
try {
|
|
162
|
+
return NodeMachineId.machineIdSync(true);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logAnalyticsError('resolveDeviceId', error);
|
|
166
|
+
return randomUUID();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function toSafeAnalyticsResult(result, operation) {
|
|
170
|
+
const resultPromise = extractResultPromise(result);
|
|
171
|
+
if (!resultPromise) {
|
|
172
|
+
return { promise: Promise.resolve() };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
promise: resultPromise
|
|
176
|
+
.then(() => undefined)
|
|
177
|
+
.catch((error) => {
|
|
178
|
+
logAnalyticsError(operation, error);
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function extractResultPromise(result) {
|
|
183
|
+
if (!result || typeof result !== 'object') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const candidate = result.promise;
|
|
187
|
+
if (candidate instanceof Promise) {
|
|
188
|
+
return candidate;
|
|
189
|
+
}
|
|
190
|
+
if (candidate && typeof candidate.then === 'function') {
|
|
191
|
+
return candidate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function logAnalyticsError(operation, error) {
|
|
195
|
+
debugLogger('Analytics operation failed (%s): %s', operation, getErrorMessage(error));
|
|
196
|
+
}
|
|
197
|
+
function clearIdentityFromContext(context) {
|
|
198
|
+
const nextContext = { ...context };
|
|
199
|
+
for (const field of CONTEXT_IDENTITY_FIELDS) {
|
|
200
|
+
delete nextContext[field];
|
|
39
201
|
}
|
|
40
|
-
return
|
|
202
|
+
return nextContext;
|
|
41
203
|
}
|
|
42
204
|
function getCIProvider(env = process.env) {
|
|
43
205
|
if (env.GITHUB_ACTIONS)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const DEFAULT_REALM_URL = 'https://idp.prod.apps.herodevs.io/realms/universe/protocol/openid-connect';
|
|
2
|
+
const DEFAULT_CLIENT_ID = 'eol-ds';
|
|
3
|
+
export function getRealmUrl() {
|
|
4
|
+
return process.env.OAUTH_CONNECT_URL || DEFAULT_REALM_URL;
|
|
5
|
+
}
|
|
6
|
+
export function getClientId() {
|
|
7
|
+
return process.env.OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TokenResponse } from '../types/auth.ts';
|
|
2
|
+
interface AuthOptions {
|
|
3
|
+
clientId?: string;
|
|
4
|
+
realmUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function refreshTokens(refreshToken: string, options?: AuthOptions): Promise<TokenResponse>;
|
|
7
|
+
export declare function logoutFromProvider(refreshToken: string | undefined, options?: AuthOptions): Promise<void>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getClientId, getRealmUrl } from "./auth-config.svc.js";
|
|
2
|
+
export async function refreshTokens(refreshToken, options = {}) {
|
|
3
|
+
const clientId = options.clientId ?? getClientId();
|
|
4
|
+
const realmUrl = options.realmUrl ?? getRealmUrl();
|
|
5
|
+
const tokenUrl = `${realmUrl}/token`;
|
|
6
|
+
const body = new URLSearchParams({
|
|
7
|
+
grant_type: 'refresh_token',
|
|
8
|
+
client_id: clientId,
|
|
9
|
+
refresh_token: refreshToken,
|
|
10
|
+
});
|
|
11
|
+
const response = await fetch(tokenUrl, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
14
|
+
body: body.toString(),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}\n${text}`);
|
|
19
|
+
}
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
22
|
+
export async function logoutFromProvider(refreshToken, options = {}) {
|
|
23
|
+
if (!refreshToken) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const clientId = options.clientId ?? getClientId();
|
|
27
|
+
const realmUrl = options.realmUrl ?? getRealmUrl();
|
|
28
|
+
const logoutUrl = `${realmUrl}/logout`;
|
|
29
|
+
const body = new URLSearchParams({
|
|
30
|
+
client_id: clientId,
|
|
31
|
+
refresh_token: refreshToken,
|
|
32
|
+
});
|
|
33
|
+
const response = await fetch(logoutUrl, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
36
|
+
body: body.toString(),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
if (response.status === 400 && text.includes('invalid_grant')) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Logout failed: ${response.status} ${response.statusText}\n${text}`);
|
|
44
|
+
}
|
|
45
|
+
}
|