@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
package/dist/api/client.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
import { ApolloError } from "../service/error.svc.js";
|
|
3
|
-
export const createApollo = (url) => new apollo.ApolloClient({
|
|
4
|
-
cache: new apollo.InMemoryCache({
|
|
5
|
-
addTypename: false,
|
|
6
|
-
}),
|
|
7
|
-
headers: {
|
|
8
|
-
'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`,
|
|
9
|
-
},
|
|
10
|
-
link: apollo.ApolloLink.from([
|
|
11
|
-
new apollo.HttpLink({
|
|
12
|
-
uri: url,
|
|
13
|
-
}),
|
|
14
|
-
]),
|
|
15
|
-
});
|
|
16
|
-
export class ApolloClient {
|
|
17
|
-
#apollo;
|
|
18
|
-
constructor(url) {
|
|
19
|
-
this.#apollo = createApollo(url);
|
|
20
|
-
}
|
|
21
|
-
async mutate(mutation, variables) {
|
|
22
|
-
try {
|
|
23
|
-
return await this.#apollo.mutate({
|
|
24
|
-
mutation,
|
|
25
|
-
variables,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
throw new ApolloError('GraphQL mutation failed', error);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async query(query, variables) {
|
|
33
|
-
try {
|
|
34
|
-
return await this.#apollo.query({
|
|
35
|
-
query,
|
|
36
|
-
variables,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
throw new ApolloError('GraphQL query failed', error);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type * as apollo from '@apollo/client/core/index.js';
|
|
2
|
-
import type { InsightsEolScanInput, InsightsEolScanResult } from '../../api/types/nes.types.ts';
|
|
3
|
-
import { type ProcessBatchOptions, type ScanInputOptions, type ScanResult } from '../types/hd-cli.types.ts';
|
|
4
|
-
export interface NesClient {
|
|
5
|
-
scan: {
|
|
6
|
-
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export declare class NesApolloClient implements NesClient {
|
|
10
|
-
#private;
|
|
11
|
-
scan: {
|
|
12
|
-
purls: (purls: string[], options: ScanInputOptions) => Promise<InsightsEolScanResult>;
|
|
13
|
-
};
|
|
14
|
-
constructor(url: string);
|
|
15
|
-
mutate<T, V extends Record<string, unknown>>(mutation: apollo.DocumentNode, variables?: V): Promise<apollo.FetchResult<T>>;
|
|
16
|
-
query<T, V extends Record<string, unknown> | undefined>(query: apollo.DocumentNode, variables?: V): Promise<apollo.ApolloQueryResult<T>>;
|
|
17
|
-
}
|
|
18
|
-
export declare const batchSubmitPurls: (purls: string[], options?: ScanInputOptions, batchSize?: number) => Promise<ScanResult>;
|
|
19
|
-
export declare const createBatches: (items: string[], batchSize: number) => string[][];
|
|
20
|
-
export declare const processBatch: ({ batch, index, totalPages, scanOptions, previousScanId, }: ProcessBatchOptions) => Promise<InsightsEolScanResult>;
|
|
21
|
-
export declare const processBatches: (batches: string[][], scanOptions: ScanInputOptions) => Promise<InsightsEolScanResult[]>;
|
|
22
|
-
export declare const handleBatchResults: (results: InsightsEolScanResult[]) => ScanResult;
|
|
23
|
-
export declare const buildInsightsEolScanInput: (purls: string[], options: ScanInputOptions) => InsightsEolScanInput;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { ApolloClient } from "../../api/client.js";
|
|
2
|
-
import { config } from "../../config/constants.js";
|
|
3
|
-
import { debugLogger } from "../../service/log.svc.js";
|
|
4
|
-
import { SbomScanner, buildScanResult } from "../../service/nes/nes.svc.js";
|
|
5
|
-
import { DEFAULT_SCAN_BATCH_SIZE, DEFAULT_SCAN_INPUT_OPTIONS, } from "../types/hd-cli.types.js";
|
|
6
|
-
export class NesApolloClient {
|
|
7
|
-
scan = {
|
|
8
|
-
purls: SbomScanner(this),
|
|
9
|
-
};
|
|
10
|
-
#apollo;
|
|
11
|
-
constructor(url) {
|
|
12
|
-
this.#apollo = new ApolloClient(url);
|
|
13
|
-
}
|
|
14
|
-
mutate(mutation, variables) {
|
|
15
|
-
return this.#apollo.mutate(mutation, variables);
|
|
16
|
-
}
|
|
17
|
-
query(query, variables) {
|
|
18
|
-
return this.#apollo.query(query, variables);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Submit a scan for a list of purls after they've been batched by batchSubmitPurls
|
|
23
|
-
*/
|
|
24
|
-
function submitScan(purls, options) {
|
|
25
|
-
const host = config.graphqlHost;
|
|
26
|
-
const path = config.graphqlPath;
|
|
27
|
-
const url = host + path;
|
|
28
|
-
const client = new NesApolloClient(url);
|
|
29
|
-
return client.scan.purls(purls, options);
|
|
30
|
-
}
|
|
31
|
-
export const batchSubmitPurls = async (purls, options = DEFAULT_SCAN_INPUT_OPTIONS, batchSize = DEFAULT_SCAN_BATCH_SIZE) => {
|
|
32
|
-
try {
|
|
33
|
-
const batches = createBatches(purls, batchSize);
|
|
34
|
-
debugLogger('Processing %d batches', batches.length);
|
|
35
|
-
if (batches.length === 0) {
|
|
36
|
-
return {
|
|
37
|
-
components: new Map(),
|
|
38
|
-
message: 'No batches to process',
|
|
39
|
-
success: true,
|
|
40
|
-
warnings: [],
|
|
41
|
-
scanId: undefined,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const results = await processBatches(batches, options);
|
|
45
|
-
return handleBatchResults(results);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
debugLogger('Fatal error in batchSubmitPurls: %s', error);
|
|
49
|
-
throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
export const createBatches = (items, batchSize) => {
|
|
53
|
-
const numberOfBatches = Math.ceil(items.length / batchSize);
|
|
54
|
-
return Array.from({ length: numberOfBatches }, (_, index) => {
|
|
55
|
-
const startIndex = index * batchSize;
|
|
56
|
-
const endIndex = startIndex + batchSize;
|
|
57
|
-
return items.slice(startIndex, endIndex);
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
export const processBatch = async ({ batch, index, totalPages, scanOptions, previousScanId, }) => {
|
|
61
|
-
const page = index + 1;
|
|
62
|
-
if (page > totalPages) {
|
|
63
|
-
throw new Error('Total pages exceeded');
|
|
64
|
-
}
|
|
65
|
-
debugLogger('Processing batch %d of %d', page, totalPages);
|
|
66
|
-
debugLogger('ScanID: %s', previousScanId);
|
|
67
|
-
const result = await submitScan(batch, {
|
|
68
|
-
...scanOptions,
|
|
69
|
-
page,
|
|
70
|
-
totalPages,
|
|
71
|
-
scanId: previousScanId,
|
|
72
|
-
});
|
|
73
|
-
return result;
|
|
74
|
-
};
|
|
75
|
-
export const processBatches = async (batches, scanOptions) => {
|
|
76
|
-
const totalPages = batches.length;
|
|
77
|
-
const results = [];
|
|
78
|
-
for (const [index, batch] of batches.entries()) {
|
|
79
|
-
const previousScanId = results[index - 1]?.scanId;
|
|
80
|
-
const result = await processBatch({
|
|
81
|
-
batch,
|
|
82
|
-
index,
|
|
83
|
-
totalPages,
|
|
84
|
-
scanOptions,
|
|
85
|
-
previousScanId,
|
|
86
|
-
});
|
|
87
|
-
results.push(result);
|
|
88
|
-
}
|
|
89
|
-
return results;
|
|
90
|
-
};
|
|
91
|
-
export const handleBatchResults = (results) => {
|
|
92
|
-
if (results.length === 0) {
|
|
93
|
-
throw new Error('No results to process');
|
|
94
|
-
}
|
|
95
|
-
// The API returns placeholders for each batch except the last one.
|
|
96
|
-
const finalResult = results[results.length - 1];
|
|
97
|
-
return buildScanResult(finalResult);
|
|
98
|
-
};
|
|
99
|
-
export const buildInsightsEolScanInput = (purls, options) => {
|
|
100
|
-
const { type, page, totalPages } = options;
|
|
101
|
-
return {
|
|
102
|
-
components: purls,
|
|
103
|
-
type,
|
|
104
|
-
page,
|
|
105
|
-
totalPages,
|
|
106
|
-
};
|
|
107
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core/core.cjs';
|
|
2
|
-
export const M_SCAN = {
|
|
3
|
-
gql: gql `
|
|
4
|
-
mutation EolScan($input: InsightsEolScanInput!) {
|
|
5
|
-
insights {
|
|
6
|
-
scan {
|
|
7
|
-
eol(input: $input) {
|
|
8
|
-
components {
|
|
9
|
-
purl
|
|
10
|
-
info {
|
|
11
|
-
isEol
|
|
12
|
-
isUnsafe
|
|
13
|
-
eolAt
|
|
14
|
-
daysEol
|
|
15
|
-
status
|
|
16
|
-
vulnCount
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
diagnostics
|
|
20
|
-
message
|
|
21
|
-
scanId
|
|
22
|
-
success
|
|
23
|
-
warnings {
|
|
24
|
-
purl
|
|
25
|
-
type
|
|
26
|
-
message
|
|
27
|
-
error
|
|
28
|
-
diagnostics
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
`,
|
|
35
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core/core.cjs';
|
|
2
|
-
export const TELEMETRY_INITIALIZE_MUTATION = gql `
|
|
3
|
-
mutation Telemetry($clientName: String!) {
|
|
4
|
-
telemetry {
|
|
5
|
-
initialize(input: { context: { client: { id: $clientName } } }) {
|
|
6
|
-
success
|
|
7
|
-
oid
|
|
8
|
-
message
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
`;
|
|
13
|
-
export const TELEMETRY_REPORT_MUTATION = gql `
|
|
14
|
-
mutation Report($key: String!, $report: JSON!, $metadata: JSON) {
|
|
15
|
-
telemetry {
|
|
16
|
-
report(input: { key: $key, report: $report, metadata: $metadata }) {
|
|
17
|
-
txId
|
|
18
|
-
success
|
|
19
|
-
message
|
|
20
|
-
diagnostics
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { type ComponentStatus, type InsightsEolScanComponent, type ScanWarning } from './nes.types.ts';
|
|
2
|
-
export declare const isValidComponentStatus: (status: string) => status is ComponentStatus;
|
|
3
|
-
export interface ScanInputOptions {
|
|
4
|
-
type: 'SBOM' | 'OTHER';
|
|
5
|
-
page: number;
|
|
6
|
-
totalPages: number;
|
|
7
|
-
scanId?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare const DEFAULT_SCAN_BATCH_SIZE = 1000;
|
|
10
|
-
export declare const DEFAULT_SCAN_INPUT_OPTIONS: ScanInputOptions;
|
|
11
|
-
export type ScanResultComponentsMap = Map<string, InsightsEolScanComponent>;
|
|
12
|
-
export type ScanInput = {
|
|
13
|
-
components: string[];
|
|
14
|
-
options: ScanInputOptions;
|
|
15
|
-
};
|
|
16
|
-
export interface ScanResult {
|
|
17
|
-
components: ScanResultComponentsMap;
|
|
18
|
-
diagnostics?: Record<string, unknown>;
|
|
19
|
-
message: string;
|
|
20
|
-
success: boolean;
|
|
21
|
-
warnings: ScanWarning[];
|
|
22
|
-
scanId: string | undefined;
|
|
23
|
-
}
|
|
24
|
-
export interface ProcessBatchOptions {
|
|
25
|
-
batch: string[];
|
|
26
|
-
index: number;
|
|
27
|
-
totalPages: number;
|
|
28
|
-
scanOptions: ScanInputOptions;
|
|
29
|
-
previousScanId?: string;
|
|
30
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { VALID_STATUSES } from "./nes.types.js";
|
|
2
|
-
export const isValidComponentStatus = (status) => {
|
|
3
|
-
return VALID_STATUSES.includes(status);
|
|
4
|
-
};
|
|
5
|
-
export const DEFAULT_SCAN_BATCH_SIZE = 1000;
|
|
6
|
-
export const DEFAULT_SCAN_INPUT_OPTIONS = {
|
|
7
|
-
type: 'SBOM',
|
|
8
|
-
page: 1,
|
|
9
|
-
totalPages: 1,
|
|
10
|
-
};
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input parameters for the EOL scan operation
|
|
3
|
-
*/
|
|
4
|
-
export interface InsightsEolScanInput {
|
|
5
|
-
scanId?: string;
|
|
6
|
-
/** Array of package URLs in purl format to scan */
|
|
7
|
-
components: string[];
|
|
8
|
-
/** The type of scan being performed (e.g. 'SBOM') */
|
|
9
|
-
type: string;
|
|
10
|
-
page: number;
|
|
11
|
-
totalPages: number;
|
|
12
|
-
}
|
|
13
|
-
export interface ScanResponse {
|
|
14
|
-
insights: {
|
|
15
|
-
scan: {
|
|
16
|
-
eol: InsightsEolScanResult;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Result of the EOL scan operation
|
|
22
|
-
*/
|
|
23
|
-
export interface InsightsEolScanResult {
|
|
24
|
-
scanId?: string;
|
|
25
|
-
success: boolean;
|
|
26
|
-
message: string;
|
|
27
|
-
components: InsightsEolScanComponent[];
|
|
28
|
-
warnings: ScanWarning[];
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Information about a component's EOL status
|
|
32
|
-
*/
|
|
33
|
-
export interface InsightsEolScanComponentInfo {
|
|
34
|
-
isEol: boolean;
|
|
35
|
-
isUnsafe: boolean;
|
|
36
|
-
eolAt: Date | null;
|
|
37
|
-
status: ComponentStatus;
|
|
38
|
-
daysEol: number | null;
|
|
39
|
-
vulnCount: number | null;
|
|
40
|
-
}
|
|
41
|
-
export interface InsightsEolScanComponent {
|
|
42
|
-
info: InsightsEolScanComponentInfo;
|
|
43
|
-
purl: string;
|
|
44
|
-
}
|
|
45
|
-
export interface ScanWarning {
|
|
46
|
-
purl: string;
|
|
47
|
-
message: string;
|
|
48
|
-
type?: string;
|
|
49
|
-
error?: unknown;
|
|
50
|
-
diagnostics?: Record<string, unknown>;
|
|
51
|
-
}
|
|
52
|
-
export type ComponentStatus = (typeof VALID_STATUSES)[number];
|
|
53
|
-
export declare const VALID_STATUSES: readonly ["UNKNOWN", "OK", "EOL", "SUPPORTED"];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'SUPPORTED'];
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
import { type ReportData } from '../../service/committers.svc.ts';
|
|
3
|
-
export default class Committers extends Command {
|
|
4
|
-
static description: string;
|
|
5
|
-
static enableJsonFlag: boolean;
|
|
6
|
-
static examples: string[];
|
|
7
|
-
static flags: {
|
|
8
|
-
months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
-
};
|
|
12
|
-
run(): Promise<ReportData | string>;
|
|
13
|
-
/**
|
|
14
|
-
* Generates structured report data
|
|
15
|
-
* @param entries - parsed git log output for commits
|
|
16
|
-
*/
|
|
17
|
-
private generateReportData;
|
|
18
|
-
/**
|
|
19
|
-
* Fetches git commit data with month and author information
|
|
20
|
-
* @param sinceDate - Date range for git log
|
|
21
|
-
*/
|
|
22
|
-
private fetchGitCommitData;
|
|
23
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { Command, Flags } from '@oclif/core';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { calculateOverallStats, formatAsCsv, formatAsText, groupCommitsByMonth, parseGitLogOutput, } from "../../service/committers.svc.js";
|
|
6
|
-
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
7
|
-
export default class Committers extends Command {
|
|
8
|
-
static description = 'Generate report of committers to a git repository';
|
|
9
|
-
static enableJsonFlag = true;
|
|
10
|
-
static examples = [
|
|
11
|
-
'<%= config.bin %> <%= command.id %>',
|
|
12
|
-
'<%= config.bin %> <%= command.id %> --csv -s',
|
|
13
|
-
'<%= config.bin %> <%= command.id %> --json',
|
|
14
|
-
'<%= config.bin %> <%= command.id %> --csv',
|
|
15
|
-
];
|
|
16
|
-
static flags = {
|
|
17
|
-
months: Flags.integer({
|
|
18
|
-
char: 'm',
|
|
19
|
-
description: 'The number of months of git history to review',
|
|
20
|
-
default: 12,
|
|
21
|
-
}),
|
|
22
|
-
csv: Flags.boolean({
|
|
23
|
-
char: 'c',
|
|
24
|
-
description: 'Output in CSV format',
|
|
25
|
-
default: false,
|
|
26
|
-
}),
|
|
27
|
-
save: Flags.boolean({
|
|
28
|
-
char: 's',
|
|
29
|
-
description: 'Save the committers report as eol.committers.<output>',
|
|
30
|
-
default: false,
|
|
31
|
-
}),
|
|
32
|
-
};
|
|
33
|
-
async run() {
|
|
34
|
-
const { flags } = await this.parse(Committers);
|
|
35
|
-
const { months, csv, save } = flags;
|
|
36
|
-
const isJson = this.jsonEnabled();
|
|
37
|
-
const sinceDate = `${months} months ago`;
|
|
38
|
-
this.log('Starting committers report with flags: %O', flags);
|
|
39
|
-
try {
|
|
40
|
-
// Generate structured report data
|
|
41
|
-
const entries = this.fetchGitCommitData(sinceDate);
|
|
42
|
-
this.log('Fetched %d commit entries', entries.length);
|
|
43
|
-
const reportData = this.generateReportData(entries);
|
|
44
|
-
// Handle different output scenarios
|
|
45
|
-
if (isJson) {
|
|
46
|
-
// JSON mode
|
|
47
|
-
if (save) {
|
|
48
|
-
try {
|
|
49
|
-
fs.writeFileSync(path.resolve('eol.committers.json'), JSON.stringify(reportData, null, 2));
|
|
50
|
-
this.log('Report written to json');
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
this.error(`Failed to save JSON report: ${getErrorMessage(error)}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return reportData;
|
|
57
|
-
}
|
|
58
|
-
const textOutput = formatAsText(reportData);
|
|
59
|
-
if (csv) {
|
|
60
|
-
// CSV mode
|
|
61
|
-
const csvOutput = formatAsCsv(reportData);
|
|
62
|
-
if (save) {
|
|
63
|
-
try {
|
|
64
|
-
fs.writeFileSync(path.resolve('eol.committers.csv'), csvOutput);
|
|
65
|
-
this.log('Report written to csv');
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
this.error(`Failed to save CSV report: ${getErrorMessage(error)}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
this.log(textOutput);
|
|
73
|
-
}
|
|
74
|
-
return csvOutput;
|
|
75
|
-
}
|
|
76
|
-
if (save) {
|
|
77
|
-
try {
|
|
78
|
-
fs.writeFileSync(path.resolve('eol.committers.txt'), textOutput);
|
|
79
|
-
this.log('Report written to txt');
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
this.error(`Failed to save txt report: ${getErrorMessage(error)}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
this.log(textOutput);
|
|
87
|
-
}
|
|
88
|
-
return textOutput;
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Generates structured report data
|
|
96
|
-
* @param entries - parsed git log output for commits
|
|
97
|
-
*/
|
|
98
|
-
generateReportData(entries) {
|
|
99
|
-
if (entries.length === 0) {
|
|
100
|
-
return { monthly: {}, overall: { total: 0 } };
|
|
101
|
-
}
|
|
102
|
-
const monthlyData = groupCommitsByMonth(entries);
|
|
103
|
-
const overallStats = calculateOverallStats(entries);
|
|
104
|
-
const grandTotal = entries.length;
|
|
105
|
-
// Format into a structured report data object
|
|
106
|
-
const report = {
|
|
107
|
-
monthly: {},
|
|
108
|
-
overall: { ...overallStats, total: grandTotal },
|
|
109
|
-
};
|
|
110
|
-
// Add monthly totals
|
|
111
|
-
for (const [month, authors] of Object.entries(monthlyData)) {
|
|
112
|
-
const monthTotal = Object.values(authors).reduce((sum, count) => sum + count, 0);
|
|
113
|
-
report.monthly[month] = { ...authors, total: monthTotal };
|
|
114
|
-
}
|
|
115
|
-
return report;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Fetches git commit data with month and author information
|
|
119
|
-
* @param sinceDate - Date range for git log
|
|
120
|
-
*/
|
|
121
|
-
fetchGitCommitData(sinceDate) {
|
|
122
|
-
const logProcess = spawnSync('git', [
|
|
123
|
-
'log',
|
|
124
|
-
'--all', // Include committers on all branches in the repo
|
|
125
|
-
'--format="%ad|%an"', // Format: date|author
|
|
126
|
-
'--date=format:%Y-%m', // Format date as YYYY-MM
|
|
127
|
-
`--since="${sinceDate}"`,
|
|
128
|
-
], { encoding: 'utf-8' });
|
|
129
|
-
if (logProcess.error) {
|
|
130
|
-
if (isErrnoException(logProcess.error)) {
|
|
131
|
-
if (logProcess.error.code === 'ENOENT') {
|
|
132
|
-
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
|
|
133
|
-
}
|
|
134
|
-
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
135
|
-
}
|
|
136
|
-
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
|
|
137
|
-
}
|
|
138
|
-
if (logProcess.status !== 0) {
|
|
139
|
-
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
|
|
140
|
-
}
|
|
141
|
-
if (!logProcess.stdout) {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
return parseGitLogOutput(logProcess.stdout);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
export default class ReportPurls extends Command {
|
|
3
|
-
static description: string;
|
|
4
|
-
static enableJsonFlag: boolean;
|
|
5
|
-
static examples: string[];
|
|
6
|
-
static flags: {
|
|
7
|
-
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
-
};
|
|
12
|
-
run(): Promise<{
|
|
13
|
-
purls: string[];
|
|
14
|
-
}>;
|
|
15
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { Command, Flags, ux } from '@oclif/core';
|
|
4
|
-
import { getErrorMessage, isErrnoException } from "../../service/error.svc.js";
|
|
5
|
-
import { extractPurls, getPurlOutput } from "../../service/purls.svc.js";
|
|
6
|
-
import ScanSbom from "../scan/sbom.js";
|
|
7
|
-
export default class ReportPurls extends Command {
|
|
8
|
-
static description = 'Generate a list of purls from a sbom';
|
|
9
|
-
static enableJsonFlag = true;
|
|
10
|
-
static examples = [
|
|
11
|
-
'<%= config.bin %> <%= command.id %> --json -s',
|
|
12
|
-
'<%= config.bin %> <%= command.id %> --dir=./my-project',
|
|
13
|
-
'<%= config.bin %> <%= command.id %> --file=path/to/sbom.json',
|
|
14
|
-
'<%= config.bin %> <%= command.id %> --dir=./my-project --save',
|
|
15
|
-
'<%= config.bin %> <%= command.id %> --save --csv',
|
|
16
|
-
];
|
|
17
|
-
static flags = {
|
|
18
|
-
file: Flags.string({
|
|
19
|
-
char: 'f',
|
|
20
|
-
description: 'The file path of an existing cyclonedx sbom to scan for EOL',
|
|
21
|
-
}),
|
|
22
|
-
dir: Flags.string({
|
|
23
|
-
char: 'd',
|
|
24
|
-
description: 'The directory to scan in order to create a cyclonedx sbom',
|
|
25
|
-
}),
|
|
26
|
-
save: Flags.boolean({
|
|
27
|
-
char: 's',
|
|
28
|
-
default: false,
|
|
29
|
-
description: 'Save the list of purls as eol.purls.<output>',
|
|
30
|
-
}),
|
|
31
|
-
csv: Flags.boolean({
|
|
32
|
-
char: 'c',
|
|
33
|
-
default: false,
|
|
34
|
-
description: 'Save output in CSV format (only applies when using --save)',
|
|
35
|
-
}),
|
|
36
|
-
};
|
|
37
|
-
async run() {
|
|
38
|
-
const { flags } = await this.parse(ReportPurls);
|
|
39
|
-
const { dir: _dirFlag, file: _fileFlag, save, csv } = flags;
|
|
40
|
-
try {
|
|
41
|
-
const sbom = await ScanSbom.loadSbom(flags, this.config);
|
|
42
|
-
const purls = await extractPurls(sbom);
|
|
43
|
-
this.log('Extracted %d purls from SBOM', purls.length);
|
|
44
|
-
ux.action.stop('Scan completed');
|
|
45
|
-
// Print the purls
|
|
46
|
-
for (const purl of purls) {
|
|
47
|
-
this.log(purl);
|
|
48
|
-
}
|
|
49
|
-
// Save if requested
|
|
50
|
-
if (save) {
|
|
51
|
-
try {
|
|
52
|
-
const outputFile = csv && !this.jsonEnabled() ? 'csv' : 'json';
|
|
53
|
-
const outputPath = path.join(_dirFlag || process.cwd(), `eol.purls.${outputFile}`);
|
|
54
|
-
const purlOutput = getPurlOutput(purls, outputFile);
|
|
55
|
-
fs.writeFileSync(outputPath, purlOutput);
|
|
56
|
-
this.log('Purls saved to %s', outputPath);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (isErrnoException(error)) {
|
|
60
|
-
switch (error.code) {
|
|
61
|
-
case 'EACCES':
|
|
62
|
-
this.error('Permission denied: Cannot write to output file');
|
|
63
|
-
break;
|
|
64
|
-
case 'ENOSPC':
|
|
65
|
-
this.error('No space left on device');
|
|
66
|
-
break;
|
|
67
|
-
case 'EISDIR':
|
|
68
|
-
this.error('Cannot write to output file: Is a directory');
|
|
69
|
-
break;
|
|
70
|
-
default:
|
|
71
|
-
this.error(`Failed to save purls: ${getErrorMessage(error)}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
this.error(`Failed to save purls: ${getErrorMessage(error)}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Return wrapped object with metadata
|
|
78
|
-
return { purls };
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
this.error(`Failed to generate PURLs: ${getErrorMessage(error)}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
import type { Sbom } from '../../service/eol/cdx.svc.ts';
|
|
3
|
-
export default class ScanSbom extends Command {
|
|
4
|
-
static description: string;
|
|
5
|
-
static enableJsonFlag: boolean;
|
|
6
|
-
static examples: string[];
|
|
7
|
-
static flags: {
|
|
8
|
-
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
-
background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
-
};
|
|
13
|
-
static loadSbom(flags: Record<string, string>, config: Command['config']): Promise<Sbom>;
|
|
14
|
-
static getSbomArgs(flags: Record<string, string>): string[];
|
|
15
|
-
getScanOptions(): {};
|
|
16
|
-
run(): Promise<Sbom | undefined>;
|
|
17
|
-
private _getSbomFromScan;
|
|
18
|
-
private _getSbomInBackground;
|
|
19
|
-
private _getSbomFromFile;
|
|
20
|
-
private _saveSbom;
|
|
21
|
-
}
|