@soos-io/soos-scm-audit 0.1.3 → 0.2.0-pre.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.
Files changed (34) hide show
  1. package/bin/constants.d.ts +8 -0
  2. package/bin/constants.js +11 -0
  3. package/bin/enums.d.ts +8 -0
  4. package/bin/enums.js +13 -0
  5. package/bin/index.js +5 -4
  6. package/bin/services/ContributorAuditArgumentParser.d.ts +17 -0
  7. package/bin/services/ContributorAuditArgumentParser.js +49 -0
  8. package/bin/services/ContributorAuditService/ContributorAuditService.d.ts +18 -0
  9. package/bin/services/ContributorAuditService/ContributorAuditService.js +88 -0
  10. package/bin/services/ContributorAuditService/index.d.ts +2 -0
  11. package/bin/services/ContributorAuditService/index.js +5 -0
  12. package/bin/services/ContributorAuditService/providers/BitbucketCloud/BitbucketCloudApiClient.d.ts +53 -0
  13. package/bin/services/ContributorAuditService/providers/BitbucketCloud/BitbucketCloudApiClient.js +120 -0
  14. package/bin/services/ContributorAuditService/providers/BitbucketCloud/BitbucketCloudContributorAuditProvider.d.ts +15 -0
  15. package/bin/services/ContributorAuditService/providers/BitbucketCloud/BitbucketCloudContributorAuditProvider.js +44 -0
  16. package/bin/services/ContributorAuditService/providers/BitbucketCloud/constants.d.ts +12 -0
  17. package/bin/services/ContributorAuditService/providers/BitbucketCloud/constants.js +15 -0
  18. package/bin/services/ContributorAuditService/providers/BitbucketCloud/index.d.ts +2 -0
  19. package/bin/services/ContributorAuditService/providers/BitbucketCloud/index.js +5 -0
  20. package/bin/services/ContributorAuditService/providers/GitHub/GitHubApiClient.d.ts +36 -0
  21. package/bin/services/ContributorAuditService/providers/GitHub/GitHubApiClient.js +124 -0
  22. package/bin/services/ContributorAuditService/providers/GitHub/GitHubContributorAuditProvider.d.ts +14 -0
  23. package/bin/services/ContributorAuditService/providers/GitHub/GitHubContributorAuditProvider.js +46 -0
  24. package/bin/services/ContributorAuditService/providers/GitHub/constants.d.ts +11 -0
  25. package/bin/services/ContributorAuditService/providers/GitHub/constants.js +14 -0
  26. package/bin/services/ContributorAuditService/providers/GitHub/index.d.ts +2 -0
  27. package/bin/services/ContributorAuditService/providers/GitHub/index.js +5 -0
  28. package/bin/services/ContributorAuditService/providers/index.d.ts +2 -0
  29. package/bin/services/ContributorAuditService/providers/index.js +5 -0
  30. package/bin/services/ContributorAuditService/utilities.d.ts +10 -0
  31. package/bin/services/ContributorAuditService/utilities.js +57 -0
  32. package/bin/services/index.d.ts +2 -0
  33. package/bin/services/index.js +5 -0
  34. package/package.json +3 -4
@@ -0,0 +1,8 @@
1
+ export declare const SOOS_SCM_AUDIT_CONSTANTS: {
2
+ Parameters: {
3
+ DefaultDaysAgo: number;
4
+ };
5
+ Files: {
6
+ ContributorAuditResults: string;
7
+ };
8
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOOS_SCM_AUDIT_CONSTANTS = void 0;
4
+ exports.SOOS_SCM_AUDIT_CONSTANTS = {
5
+ Parameters: {
6
+ DefaultDaysAgo: 90,
7
+ },
8
+ Files: {
9
+ ContributorAuditResults: "soos_contributor_audit",
10
+ },
11
+ };
package/bin/enums.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare enum ScmResultsFormat {
2
+ JSON = "JSON",
3
+ TXT = "TXT"
4
+ }
5
+ export declare enum ScmType {
6
+ GitHub = "GitHub",
7
+ BitbucketCloud = "BitbucketCloud"
8
+ }
package/bin/enums.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScmType = exports.ScmResultsFormat = void 0;
4
+ var ScmResultsFormat;
5
+ (function (ScmResultsFormat) {
6
+ ScmResultsFormat["JSON"] = "JSON";
7
+ ScmResultsFormat["TXT"] = "TXT";
8
+ })(ScmResultsFormat || (exports.ScmResultsFormat = ScmResultsFormat = {}));
9
+ var ScmType;
10
+ (function (ScmType) {
11
+ ScmType["GitHub"] = "GitHub";
12
+ ScmType["BitbucketCloud"] = "BitbucketCloud";
13
+ })(ScmType || (exports.ScmType = ScmType = {}));
package/bin/index.js CHANGED
@@ -6,8 +6,9 @@ const package_json_1 = require("../package.json");
6
6
  const api_client_1 = require("@soos-io/api-client");
7
7
  const process_1 = require("process");
8
8
  const utilities_1 = require("@soos-io/api-client/dist/utilities");
9
- const ContributorAuditService_1 = tslib_1.__importDefault(require("@soos-io/api-client/dist/services/ContributorAuditService/ContributorAuditService"));
10
- const ContributorAuditArgumentParser_1 = tslib_1.__importDefault(require("@soos-io/api-client/dist/services/ContributorAuditArgumentParser"));
9
+ const ContributorAuditArgumentParser_1 = tslib_1.__importDefault(require("./services/ContributorAuditArgumentParser"));
10
+ const ContributorAuditService_1 = tslib_1.__importDefault(require("./services/ContributorAuditService/ContributorAuditService"));
11
+ const enums_1 = require("./enums");
11
12
  class SOOSSCMAudit {
12
13
  args;
13
14
  constructor(args) {
@@ -21,7 +22,7 @@ class SOOSSCMAudit {
21
22
  async runAudit() {
22
23
  const contributingDeveloperService = ContributorAuditService_1.default.create(this.args.apiKey, this.args.apiURL, this.args.scmType);
23
24
  let auditParams;
24
- if (this.args.scmType === api_client_1.ScmType.GitHub) {
25
+ if (this.args.scmType === enums_1.ScmType.GitHub) {
25
26
  const githubArgs = this.args;
26
27
  auditParams = {
27
28
  days: this.args.days,
@@ -30,7 +31,7 @@ class SOOSSCMAudit {
30
31
  secret: this.args.secret,
31
32
  };
32
33
  }
33
- else if (this.args.scmType === api_client_1.ScmType.BitbucketCloud) {
34
+ else if (this.args.scmType === enums_1.ScmType.BitbucketCloud) {
34
35
  const bitbucketCloudArgs = this.args;
35
36
  auditParams = {
36
37
  days: this.args.days,
@@ -0,0 +1,17 @@
1
+ import { ArgumentParserBase, ICommonArguments } from "@soos-io/api-client";
2
+ import { ScmResultsFormat, ScmType } from "../enums";
3
+ import { Command, OptionValues } from "commander";
4
+ interface IContributorAuditArguments extends ICommonArguments {
5
+ days: number;
6
+ secret: string;
7
+ saveResults: ScmResultsFormat;
8
+ scmType: ScmType;
9
+ }
10
+ declare class ContributorAuditArgumentParser extends ArgumentParserBase {
11
+ constructor(argumentParser: Command);
12
+ static create(): ContributorAuditArgumentParser;
13
+ addBaseContributorArguments(): void;
14
+ parseArguments<T extends OptionValues>(): T;
15
+ }
16
+ export default ContributorAuditArgumentParser;
17
+ export { IContributorAuditArguments };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const GitHubContributorAuditProvider_1 = tslib_1.__importDefault(require("./ContributorAuditService/providers/GitHub/GitHubContributorAuditProvider"));
5
+ const BitbucketCloudContributorAuditProvider_1 = tslib_1.__importDefault(require("./ContributorAuditService/providers/BitbucketCloud/BitbucketCloudContributorAuditProvider"));
6
+ const api_client_1 = require("@soos-io/api-client");
7
+ const enums_1 = require("../enums");
8
+ const commander_1 = require("commander");
9
+ const constants_1 = require("../constants");
10
+ class ContributorAuditArgumentParser extends api_client_1.ArgumentParserBase {
11
+ constructor(argumentParser) {
12
+ super(argumentParser);
13
+ }
14
+ static create() {
15
+ const parser = commander_1.program.description("SOOS SCM Audit");
16
+ return new ContributorAuditArgumentParser(parser);
17
+ }
18
+ addBaseContributorArguments() {
19
+ this.argumentParser.option("--days", "Number of days to look back for commits.", (value) => {
20
+ const parsedValue = parseInt(value, 10);
21
+ if (isNaN(parsedValue) || parsedValue <= 0) {
22
+ throw new Error(`Invalid value for days: ${value}`);
23
+ }
24
+ return parsedValue;
25
+ }, constants_1.SOOS_SCM_AUDIT_CONSTANTS.Parameters.DefaultDaysAgo);
26
+ this.addEnumArgument("--saveResults", enums_1.ScmResultsFormat, "Save results to file, options available: JSON, TXT.", enums_1.ScmResultsFormat.TXT);
27
+ this.addEnumArgument("--scmType", enums_1.ScmType, "Scm Type to use for the audit. Options: GitHub, Bitbucket.", enums_1.ScmType.GitHub);
28
+ }
29
+ parseArguments() {
30
+ this.addCommonArguments();
31
+ this.addBaseContributorArguments();
32
+ this.argumentParser.parse();
33
+ const preProviderArgs = this.argumentParser.opts();
34
+ switch (preProviderArgs.scmType) {
35
+ case enums_1.ScmType.GitHub:
36
+ GitHubContributorAuditProvider_1.default.addProviderArgs(this.argumentParser);
37
+ break;
38
+ case enums_1.ScmType.BitbucketCloud:
39
+ BitbucketCloudContributorAuditProvider_1.default.addProviderArgs(this.argumentParser);
40
+ break;
41
+ default:
42
+ throw new Error("Unsupported scmType");
43
+ }
44
+ this.argumentParser.parse();
45
+ const args = this.argumentParser.opts();
46
+ return args;
47
+ }
48
+ }
49
+ exports.default = ContributorAuditArgumentParser;
@@ -0,0 +1,18 @@
1
+ import { ScmResultsFormat, ScmType } from "../../enums";
2
+ import SOOSHooksApiClient, { IContributorAuditModel } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
3
+ export interface IContributorAuditProvider {
4
+ audit(implementationParams: Record<string, string | number>): Promise<IContributorAuditModel>;
5
+ }
6
+ declare class ContributorAuditService {
7
+ private auditProvider;
8
+ hooksApiClient: SOOSHooksApiClient;
9
+ constructor(auditProvider: IContributorAuditProvider, hooksApiClient: SOOSHooksApiClient);
10
+ static create(apiKey: string, apiURL: string, scmType: ScmType): ContributorAuditService;
11
+ audit(implementationParams: Record<string, string | number>): Promise<IContributorAuditModel>;
12
+ uploadContributorAudits(clientHash: string, contributorAudit: IContributorAuditModel): Promise<void>;
13
+ saveResults(results: IContributorAuditModel, format: ScmResultsFormat): Promise<void>;
14
+ private saveResultsAsJSON;
15
+ private saveResultsAsTXT;
16
+ private validateCommonParams;
17
+ }
18
+ export default ContributorAuditService;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs_1 = tslib_1.__importDefault(require("fs"));
5
+ const Path = tslib_1.__importStar(require("path"));
6
+ const utilities_1 = require("./utilities");
7
+ const GitHubContributorAuditProvider_1 = tslib_1.__importDefault(require("./providers/GitHub/GitHubContributorAuditProvider"));
8
+ const BitbucketCloudContributorAuditProvider_1 = tslib_1.__importDefault(require("./providers/BitbucketCloud/BitbucketCloudContributorAuditProvider"));
9
+ const enums_1 = require("../../enums");
10
+ const SOOSHooksApiClient_1 = tslib_1.__importDefault(require("@soos-io/api-client/dist/api/SOOSHooksApiClient"));
11
+ const constants_1 = require("../../constants");
12
+ const api_client_1 = require("@soos-io/api-client");
13
+ class ContributorAuditService {
14
+ auditProvider;
15
+ hooksApiClient;
16
+ constructor(auditProvider, hooksApiClient) {
17
+ this.auditProvider = auditProvider;
18
+ this.hooksApiClient = hooksApiClient;
19
+ }
20
+ static create(apiKey, apiURL, scmType) {
21
+ let auditProvider;
22
+ switch (scmType) {
23
+ case enums_1.ScmType.GitHub: {
24
+ auditProvider = new GitHubContributorAuditProvider_1.default();
25
+ break;
26
+ }
27
+ case enums_1.ScmType.BitbucketCloud: {
28
+ auditProvider = new BitbucketCloudContributorAuditProvider_1.default();
29
+ break;
30
+ }
31
+ default: {
32
+ throw new Error(`Unsupported SCM type: ${scmType}`);
33
+ }
34
+ }
35
+ const hooksApiClient = new SOOSHooksApiClient_1.default(apiKey, apiURL.replace("api.", "api-hooks."));
36
+ return new ContributorAuditService(auditProvider, hooksApiClient);
37
+ }
38
+ async audit(implementationParams) {
39
+ this.validateCommonParams(implementationParams);
40
+ const contributors = await this.auditProvider.audit(implementationParams);
41
+ api_client_1.soosLogger.debug(`Contributing Developers found: ${JSON.stringify(contributors, null, 2)}`);
42
+ return contributors;
43
+ }
44
+ async uploadContributorAudits(clientHash, contributorAudit) {
45
+ api_client_1.soosLogger.info(`Uploading Contributor Audit to SOOS.`);
46
+ await this.hooksApiClient.postContributorAudits(clientHash, contributorAudit);
47
+ api_client_1.soosLogger.info(`Results uploaded successfully.`);
48
+ }
49
+ async saveResults(results, format) {
50
+ api_client_1.soosLogger.info(`Saving results.`);
51
+ switch (format) {
52
+ case enums_1.ScmResultsFormat.JSON: {
53
+ await this.saveResultsAsJSON(results);
54
+ break;
55
+ }
56
+ case enums_1.ScmResultsFormat.TXT: {
57
+ await this.saveResultsAsTXT(results);
58
+ break;
59
+ }
60
+ default: {
61
+ throw new Error(`Unsupported format: ${format}`);
62
+ }
63
+ }
64
+ }
65
+ async saveResultsAsJSON(results) {
66
+ fs_1.default.writeFileSync(Path.join(process.cwd(), `${constants_1.SOOS_SCM_AUDIT_CONSTANTS.Files.ContributorAuditResults}.json`), JSON.stringify(results, null, 2));
67
+ api_client_1.soosLogger.info(`Results saved successfully ${Path.join(process.cwd(), `${constants_1.SOOS_SCM_AUDIT_CONSTANTS.Files.ContributorAuditResults}.json`)}`);
68
+ }
69
+ async saveResultsAsTXT(results) {
70
+ const uniqueContributors = new Set();
71
+ results.contributors
72
+ .sort((a, b) => a.username.localeCompare(b.username))
73
+ .forEach((contributor) => {
74
+ uniqueContributors.add(contributor.username);
75
+ });
76
+ fs_1.default.writeFileSync(Path.join(process.cwd(), `${constants_1.SOOS_SCM_AUDIT_CONSTANTS.Files.ContributorAuditResults}.txt`), Array.from(uniqueContributors).join("\n"));
77
+ api_client_1.soosLogger.info(`Results saved successfully ${Path.join(process.cwd(), `${constants_1.SOOS_SCM_AUDIT_CONSTANTS.Files.ContributorAuditResults}.txt`)}`);
78
+ }
79
+ validateCommonParams(implementationParams) {
80
+ if (!implementationParams["days"]) {
81
+ throw new Error("Days is required");
82
+ }
83
+ if (utilities_1.ParamUtilities.getAsNumber(implementationParams, "days") < 0) {
84
+ throw new Error("Days must be greater than 0");
85
+ }
86
+ }
87
+ }
88
+ exports.default = ContributorAuditService;
@@ -0,0 +1,2 @@
1
+ export * from "./providers";
2
+ export * from "./ContributorAuditService";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./providers"), exports);
5
+ tslib_1.__exportStar(require("./ContributorAuditService"), exports);
@@ -0,0 +1,53 @@
1
+ import { IContributorAuditRepositories } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
2
+ export interface BitbucketRepositoryApiResponse {
3
+ values: BitbucketCloudRepository[];
4
+ next?: string;
5
+ }
6
+ export interface BitbucketCloudRepository {
7
+ uuid: string;
8
+ full_name: string;
9
+ name: string;
10
+ is_private: boolean;
11
+ updated_on: string;
12
+ workspace: BitbucketCloudWorkspace;
13
+ }
14
+ export interface BitbucketCloudWorkspace {
15
+ type: string;
16
+ uuid: string;
17
+ name: string;
18
+ slug: string;
19
+ }
20
+ export interface BitbucketCloudCommitsApiResponse {
21
+ values: BitbucketCloudCommit[];
22
+ next?: string;
23
+ }
24
+ export interface BitbucketCloudCommit {
25
+ author: BitbucketCloudAuthor;
26
+ date: string;
27
+ }
28
+ export interface BitbucketCloudAuthor {
29
+ raw: string;
30
+ name: string;
31
+ emailAddress: string;
32
+ displayName: string;
33
+ user: BitbucketCloudUser;
34
+ }
35
+ interface BitbucketCloudUser {
36
+ display_name: string;
37
+ type: string;
38
+ nickname: string;
39
+ }
40
+ declare class BitbucketCloudApiClient {
41
+ private readonly client;
42
+ private readonly workspace;
43
+ private readonly days;
44
+ private readonly dateToFilter;
45
+ constructor(days: number, username: string, password: string, workspace: string, baseUri?: string);
46
+ private static createHttpClient;
47
+ getBitbucketCloudRepositories(): Promise<BitbucketCloudRepository[]>;
48
+ getBitbucketCloudRepositoryContributors(repository: BitbucketCloudRepository): Promise<IContributorAuditRepositories[]>;
49
+ private setupInterceptor;
50
+ private static handleRepositoryPagination;
51
+ private handleCommitPagination;
52
+ }
53
+ export default BitbucketCloudApiClient;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const axios_1 = tslib_1.__importDefault(require("axios"));
5
+ const constants_1 = require("./constants");
6
+ const utilities_1 = require("../../utilities");
7
+ const utilities_2 = require("@soos-io/api-client/dist/utilities");
8
+ const api_client_1 = require("@soos-io/api-client");
9
+ class BitbucketCloudApiClient {
10
+ client;
11
+ workspace;
12
+ days;
13
+ dateToFilter;
14
+ constructor(days, username, password, workspace, baseUri = constants_1.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS.Urls.API.Base) {
15
+ this.workspace = workspace;
16
+ this.days = days;
17
+ this.dateToFilter = utilities_2.DateUtilities.getDate(this.days).toISOString();
18
+ this.client = BitbucketCloudApiClient.createHttpClient({
19
+ baseUri,
20
+ username: username,
21
+ password: password,
22
+ apiClientName: "BitbucketCloud API",
23
+ dateToFilter: this.dateToFilter,
24
+ });
25
+ }
26
+ static createHttpClient({ baseUri, username, password }) {
27
+ const client = axios_1.default.create({
28
+ baseURL: baseUri,
29
+ auth: {
30
+ username: username,
31
+ password: password,
32
+ },
33
+ });
34
+ return client;
35
+ }
36
+ async getBitbucketCloudRepositories() {
37
+ this.client.interceptors.response.use((response) => {
38
+ if (response.data.next) {
39
+ return BitbucketCloudApiClient.handleRepositoryPagination(response, this.client, this.dateToFilter);
40
+ }
41
+ return response;
42
+ });
43
+ const response = await this.client.get(`repositories/${this.workspace}`);
44
+ const repoResponse = response.data;
45
+ const repos = repoResponse.values.filter((repo) => utilities_2.DateUtilities.isWithinDateRange(new Date(repo.updated_on), new Date(this.dateToFilter)));
46
+ return repos;
47
+ }
48
+ async getBitbucketCloudRepositoryContributors(repository) {
49
+ await this.setupInterceptor(this.client, "BitbucketCloud API", this.dateToFilter, this.handleCommitPagination);
50
+ const response = await this.client.get(`repositories/${this.workspace}/${repository.name}/commits`);
51
+ response.data.values = response.data.values.filter((commit) => utilities_2.DateUtilities.isWithinDateRange(new Date(commit.date), new Date(this.dateToFilter)));
52
+ const commits = response.data;
53
+ const contributors = commits.values.reduce((acc, commit) => {
54
+ const username = commit.author.user ? commit.author.user.display_name : "Unknown Author";
55
+ const commitDate = commit.date;
56
+ const repo = {
57
+ id: repository.uuid,
58
+ name: repository.name,
59
+ lastCommit: commitDate,
60
+ isPrivate: repository.is_private,
61
+ };
62
+ return utilities_1.DataMappingUtilities.updateContributors(acc, repo, username, commitDate);
63
+ }, []);
64
+ return contributors;
65
+ }
66
+ async setupInterceptor(client, apiClientName, dateToFilter, paginationFunction) {
67
+ client.interceptors.response.clear();
68
+ client.interceptors.response.use(async (response) => {
69
+ api_client_1.soosLogger.debug(apiClientName, `Response Body: ${JSON.stringify(response.data)}`);
70
+ if (response.data.next) {
71
+ return await paginationFunction(response, client, dateToFilter);
72
+ }
73
+ return response;
74
+ }, async (error) => {
75
+ const { config, response } = error;
76
+ const maxRetries = 3;
77
+ config.retryCount = config.retryCount || 0;
78
+ if (response?.status === 429 && config.retryCount < maxRetries) {
79
+ api_client_1.soosLogger.debug(apiClientName, `Rate limit exceeded on the BitbucketCloud API. Waiting ${constants_1.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS.RetrySeconds} seconds before retrying. Retry count: ${config.retryCount}`);
80
+ config.retryCount += 1;
81
+ await (0, utilities_2.sleep)(constants_1.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS.RetrySeconds * 1000);
82
+ return client(config);
83
+ }
84
+ if (response?.status) {
85
+ api_client_1.soosLogger.debug(apiClientName, `Response Status: ${response.status}`);
86
+ }
87
+ return Promise.reject(error);
88
+ });
89
+ }
90
+ static async handleRepositoryPagination(response, client, dateToFilter) {
91
+ const data = response.data;
92
+ let nextUrl = data.next;
93
+ let isWithinDateRange = data.values.every((repo) => utilities_2.DateUtilities.isWithinDateRange(new Date(repo.updated_on), new Date(dateToFilter)));
94
+ while (nextUrl && isWithinDateRange) {
95
+ api_client_1.soosLogger.debug("Fetching next page", nextUrl);
96
+ const nextPageResponse = await client.get(nextUrl);
97
+ data.values = data.values.concat(nextPageResponse.data.values);
98
+ nextUrl = nextPageResponse.data.next ?? undefined;
99
+ isWithinDateRange = nextPageResponse.data.values.every((repo) => utilities_2.DateUtilities.isWithinDateRange(new Date(repo.updated_on), new Date(dateToFilter)));
100
+ }
101
+ return { ...response, data };
102
+ }
103
+ async handleCommitPagination(response, client, dateToFilter) {
104
+ const data = response.data;
105
+ let nextUrl = data.next;
106
+ let lastCommitDate = new Date(data.values[data.values.length - 1].date);
107
+ let isWithinDateRange = utilities_2.DateUtilities.isWithinDateRange(lastCommitDate, new Date(dateToFilter));
108
+ while (nextUrl && isWithinDateRange) {
109
+ api_client_1.soosLogger.debug("Fetching next page", nextUrl);
110
+ const nextPageResponse = await client.get(nextUrl);
111
+ data.values = data.values.concat(nextPageResponse.data.values);
112
+ api_client_1.soosLogger.debug(`Checking if commits are within date range min date ${new Date(dateToFilter)}`);
113
+ lastCommitDate = new Date(nextPageResponse.data.values[nextPageResponse.data.values.length - 1].date);
114
+ isWithinDateRange = utilities_2.DateUtilities.isWithinDateRange(lastCommitDate, new Date(dateToFilter));
115
+ nextUrl = nextPageResponse.data.next ?? undefined;
116
+ }
117
+ return { ...response, data };
118
+ }
119
+ }
120
+ exports.default = BitbucketCloudApiClient;
@@ -0,0 +1,15 @@
1
+ import { IContributorAuditProvider } from "../../ContributorAuditService";
2
+ import { IContributorAuditArguments } from "../../../ContributorAuditArgumentParser";
3
+ import { IContributorAuditModel } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
4
+ import { Command } from "commander";
5
+ interface IBitBucketContributorAuditArguments extends IContributorAuditArguments {
6
+ username: string;
7
+ workspace: string;
8
+ }
9
+ declare class BitbucketCloudContributorAuditProvider implements IContributorAuditProvider {
10
+ audit(implementationParams: Record<string, string | number>): Promise<IContributorAuditModel>;
11
+ static addProviderArgs(argumentParser: Command): void;
12
+ private getBitbucketCloudRepositoryContributors;
13
+ }
14
+ export default BitbucketCloudContributorAuditProvider;
15
+ export { IBitBucketContributorAuditArguments };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const utilities_1 = require("../../utilities");
5
+ const BitbucketCloudApiClient_1 = tslib_1.__importDefault(require("./BitbucketCloudApiClient"));
6
+ const constants_1 = require("./constants");
7
+ const api_client_1 = require("@soos-io/api-client");
8
+ class BitbucketCloudContributorAuditProvider {
9
+ async audit(implementationParams) {
10
+ const bitbucketPAT = utilities_1.ParamUtilities.getAsString(implementationParams, "secret");
11
+ const workspace = utilities_1.ParamUtilities.getAsString(implementationParams, "workspace");
12
+ const days = utilities_1.ParamUtilities.getAsNumber(implementationParams, "days");
13
+ const username = utilities_1.ParamUtilities.getAsString(implementationParams, "username");
14
+ const bitbucketApiClient = new BitbucketCloudApiClient_1.default(days, username, bitbucketPAT, workspace);
15
+ const repositories = await bitbucketApiClient.getBitbucketCloudRepositories();
16
+ api_client_1.soosLogger.debug("Fetching commits for each repository");
17
+ const contributors = await this.getBitbucketCloudRepositoryContributors(bitbucketApiClient, repositories, constants_1.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS.RequestBatchSize);
18
+ const scriptVersion = utilities_1.ParamUtilities.getAsString(implementationParams, "scriptVersion");
19
+ const finalContributors = {
20
+ metadata: {
21
+ scriptVersion: scriptVersion,
22
+ days: days,
23
+ },
24
+ organizationName: workspace,
25
+ contributors: contributors,
26
+ };
27
+ return finalContributors;
28
+ }
29
+ static addProviderArgs(argumentParser) {
30
+ argumentParser.requiredOption("--workspace", "Organization name to use for the audit.");
31
+ argumentParser.requiredOption("--secret", "Secret to use for api calls, it should be an app password.");
32
+ argumentParser.requiredOption("--username", "Username for audit.");
33
+ }
34
+ async getBitbucketCloudRepositoryContributors(bitbucketApiClient, repositories, batchSize) {
35
+ const contributorsArray = [];
36
+ for (let i = 0; i < repositories.length; i += batchSize) {
37
+ const batch = repositories.slice(i, i + batchSize);
38
+ const results = await Promise.all(batch.map((repo) => bitbucketApiClient.getBitbucketCloudRepositoryContributors(repo)));
39
+ contributorsArray.push(...results);
40
+ }
41
+ return utilities_1.DataMappingUtilities.mergeContributors(contributorsArray);
42
+ }
43
+ }
44
+ exports.default = BitbucketCloudContributorAuditProvider;
@@ -0,0 +1,12 @@
1
+ export declare const SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS: {
2
+ Urls: {
3
+ API: {
4
+ Base: string;
5
+ };
6
+ Docs: {
7
+ AppPassword: string;
8
+ };
9
+ };
10
+ RequestBatchSize: number;
11
+ RetrySeconds: number;
12
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS = void 0;
4
+ exports.SOOS_BITBUCKET_CLOUD_CONTRIBUTOR_AUDIT_CONSTANTS = {
5
+ Urls: {
6
+ API: {
7
+ Base: "https://api.bitbucket.org/2.0/",
8
+ },
9
+ Docs: {
10
+ AppPassword: "https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/",
11
+ },
12
+ },
13
+ RequestBatchSize: 10,
14
+ RetrySeconds: 60,
15
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./BitbucketCloudApiClient";
2
+ export * from "./BitbucketCloudContributorAuditProvider";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./BitbucketCloudApiClient"), exports);
5
+ tslib_1.__exportStar(require("./BitbucketCloudContributorAuditProvider"), exports);
@@ -0,0 +1,36 @@
1
+ import { IContributorAuditRepositories } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
2
+ export interface GitHubOrganization {
3
+ login: string;
4
+ }
5
+ export interface GitHubRepository {
6
+ id: number;
7
+ name: string;
8
+ full_name: string;
9
+ private: boolean;
10
+ owner: GitHubOrganization;
11
+ pushed_at: string;
12
+ }
13
+ export interface GitHubCommits {
14
+ commit: {
15
+ author: GitHubAuthor;
16
+ };
17
+ }
18
+ export interface GitHubAuthor {
19
+ name: string;
20
+ email: string;
21
+ date: string;
22
+ }
23
+ declare class GitHubApiClient {
24
+ private readonly client;
25
+ private readonly organizationName;
26
+ private readonly days;
27
+ private readonly dateToFilter;
28
+ constructor(days: number, gitHubPAT: string, organizationName: string, baseUri?: string);
29
+ private static createHttpClient;
30
+ private static handleNextPage;
31
+ private static getNextPageUrl;
32
+ getGitHubOrganizations(): Promise<GitHubOrganization[]>;
33
+ getGitHubOrganizationRepositories(organization: GitHubOrganization): Promise<GitHubRepository[]>;
34
+ getGitHubRepositoryContributors(repository: GitHubRepository): Promise<IContributorAuditRepositories[]>;
35
+ }
36
+ export default GitHubApiClient;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const axios_1 = tslib_1.__importDefault(require("axios"));
5
+ const constants_1 = require("./constants");
6
+ const utilities_1 = require("../../utilities");
7
+ const utilities_2 = require("@soos-io/api-client/dist/utilities");
8
+ const api_client_1 = require("@soos-io/api-client");
9
+ class GitHubApiClient {
10
+ client;
11
+ organizationName;
12
+ days;
13
+ dateToFilter;
14
+ constructor(days, gitHubPAT, organizationName, baseUri = constants_1.SOOS_GITHUB_CONTRIBUTOR_AUDIT_CONSTANTS.Urls.API.Base) {
15
+ this.client = GitHubApiClient.createHttpClient({
16
+ baseUri,
17
+ gitHubPAT: gitHubPAT,
18
+ apiClientName: "GitHub API",
19
+ });
20
+ this.organizationName = organizationName;
21
+ this.days = days;
22
+ this.dateToFilter = utilities_2.DateUtilities.getDate(this.days).toISOString();
23
+ }
24
+ static createHttpClient({ baseUri, gitHubPAT: gitHubPAT, apiClientName, }) {
25
+ const client = axios_1.default.create({
26
+ baseURL: baseUri,
27
+ headers: {
28
+ accept: "application/vnd.github+json",
29
+ Authorization: `Bearer ${gitHubPAT}`,
30
+ },
31
+ });
32
+ client.interceptors.request.use((request) => {
33
+ if (request.data) {
34
+ api_client_1.soosLogger.debug(apiClientName, `Request URL: ${request.method?.toLocaleUpperCase()} ${request.url}`);
35
+ if (request.params) {
36
+ api_client_1.soosLogger.debug(apiClientName, `Request Params: ${JSON.stringify(request.params)}`);
37
+ }
38
+ api_client_1.soosLogger.debug(apiClientName, `Request Body: ${JSON.stringify(request.data)}`);
39
+ }
40
+ return request;
41
+ }, (rejectedRequest) => {
42
+ return Promise.reject(rejectedRequest);
43
+ });
44
+ client.interceptors.response.use(async (response) => {
45
+ api_client_1.soosLogger.debug(apiClientName, `Response Body: ${JSON.stringify(response.data)}`);
46
+ if (response.config.url?.includes("per_page")) {
47
+ return await GitHubApiClient.handleNextPage(response, client);
48
+ }
49
+ return response;
50
+ }, async (error) => {
51
+ const { config, response } = error;
52
+ const maxRetries = 3;
53
+ config.retryCount = config.retryCount || 0;
54
+ if ((response?.status === 429 || response?.status === 403) &&
55
+ config.retryCount < maxRetries) {
56
+ const rateLimitReset = response?.headers["x-ratelimit-reset"];
57
+ if (rateLimitReset) {
58
+ api_client_1.soosLogger.debug(`Trying to parse rate limit reset: ${rateLimitReset}`);
59
+ const rateLimitDate = utilities_2.DateUtilities.getDateFromUnixUTC(rateLimitReset);
60
+ const timeToWait = Math.floor((rateLimitDate.getTime() - Date.now()) / 1000);
61
+ api_client_1.soosLogger.debug(apiClientName, `Rate limit exceeded on the GitHub API. Waiting ${timeToWait} seconds before retrying. Retry count: ${config.retryCount}`);
62
+ config.retryCount += 1;
63
+ await (0, utilities_2.sleep)(timeToWait * 1000);
64
+ }
65
+ return client(config);
66
+ }
67
+ if (response?.status) {
68
+ api_client_1.soosLogger.debug(apiClientName, `Response Status: ${response.status}`);
69
+ }
70
+ if (response?.data?.message) {
71
+ api_client_1.soosLogger.debug(apiClientName, `Response Message: ${response.data.message}`);
72
+ }
73
+ return Promise.reject(error);
74
+ });
75
+ return client;
76
+ }
77
+ static async handleNextPage(response, client) {
78
+ let data = response.data;
79
+ const nextUrl = GitHubApiClient.getNextPageUrl(response);
80
+ if (nextUrl) {
81
+ api_client_1.soosLogger.debug("Fetching next page", nextUrl);
82
+ const nextPageResponse = await client.get(nextUrl);
83
+ data = data.concat(nextPageResponse.data);
84
+ }
85
+ return { ...response, data };
86
+ }
87
+ static getNextPageUrl(response) {
88
+ const linkHeader = response.headers["link"];
89
+ const nextLink = linkHeader?.split(",").find((s) => s.includes('rel="next"'));
90
+ return nextLink
91
+ ? new URL(nextLink.split(";")[0].trim().slice(1, -1), response.config.baseURL).toString()
92
+ : null;
93
+ }
94
+ async getGitHubOrganizations() {
95
+ const response = await this.client.get(`user/orgs?per_page=100`);
96
+ const orgs = response.data.filter((org) => org.login.toLowerCase() === this.organizationName.toLowerCase());
97
+ if (orgs.length === 0) {
98
+ throw new Error(`Organization ${this.organizationName} not found`);
99
+ }
100
+ return orgs;
101
+ }
102
+ async getGitHubOrganizationRepositories(organization) {
103
+ const response = await this.client.get(`orgs/${organization.login}/repos?per_page=50`);
104
+ const repos = response.data.filter((repo) => utilities_2.DateUtilities.isWithinDateRange(new Date(repo.pushed_at), new Date(this.dateToFilter)));
105
+ return repos;
106
+ }
107
+ async getGitHubRepositoryContributors(repository) {
108
+ const response = await this.client.get(`repos/${repository.owner.login}/${repository.name}/commits?per_page=100&since=${this.dateToFilter}`);
109
+ const commits = await response.data;
110
+ const contributors = commits.reduce((acc, commit) => {
111
+ const username = commit.commit.author.name;
112
+ const commitDate = commit.commit.author.date;
113
+ const repo = {
114
+ id: repository.id.toString(),
115
+ name: repository.name,
116
+ lastCommit: commitDate,
117
+ isPrivate: repository.private,
118
+ };
119
+ return utilities_1.DataMappingUtilities.updateContributors(acc, repo, username, commitDate);
120
+ }, []);
121
+ return contributors;
122
+ }
123
+ }
124
+ exports.default = GitHubApiClient;
@@ -0,0 +1,14 @@
1
+ import { IContributorAuditProvider } from "../../ContributorAuditService";
2
+ import { IContributorAuditArguments } from "../../../ContributorAuditArgumentParser";
3
+ import { IContributorAuditModel } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
4
+ import { Command } from "commander";
5
+ interface IGitHubContributorAuditArguments extends IContributorAuditArguments {
6
+ organizationName: string;
7
+ }
8
+ declare class GitHubContributorAuditProvider implements IContributorAuditProvider {
9
+ audit(implementationParams: Record<string, string | number>): Promise<IContributorAuditModel>;
10
+ static addProviderArgs(argumentParser: Command): void;
11
+ private getGitHubRepositoryContributors;
12
+ }
13
+ export default GitHubContributorAuditProvider;
14
+ export { IGitHubContributorAuditArguments };
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const utilities_1 = require("../../utilities");
5
+ const GitHubApiClient_1 = tslib_1.__importDefault(require("./GitHubApiClient"));
6
+ const constants_1 = require("./constants");
7
+ const api_client_1 = require("@soos-io/api-client");
8
+ class GitHubContributorAuditProvider {
9
+ async audit(implementationParams) {
10
+ const gitHubPAT = utilities_1.ParamUtilities.getAsString(implementationParams, "secret");
11
+ const organizationName = utilities_1.ParamUtilities.getAsString(implementationParams, "organizationName");
12
+ const days = utilities_1.ParamUtilities.getAsNumber(implementationParams, "days");
13
+ const gitHubApiClient = new GitHubApiClient_1.default(days, gitHubPAT, organizationName);
14
+ const organizations = await gitHubApiClient.getGitHubOrganizations();
15
+ api_client_1.soosLogger.debug("Fetching GitHub repositories");
16
+ const repositories = await Promise.all(organizations.map((org) => gitHubApiClient.getGitHubOrganizationRepositories(org)));
17
+ api_client_1.soosLogger.debug("Fetching commits for each repository");
18
+ const contributors = await this.getGitHubRepositoryContributors(gitHubApiClient, repositories.flatMap((repoArray) => {
19
+ return repoArray;
20
+ }), constants_1.SOOS_GITHUB_CONTRIBUTOR_AUDIT_CONSTANTS.RequestBatchSize);
21
+ const scriptVersion = utilities_1.ParamUtilities.getAsString(implementationParams, "scriptVersion");
22
+ const finalContributors = {
23
+ metadata: {
24
+ scriptVersion: scriptVersion,
25
+ days: days,
26
+ },
27
+ organizationName: organizationName,
28
+ contributors: contributors,
29
+ };
30
+ return finalContributors;
31
+ }
32
+ static addProviderArgs(argumentParser) {
33
+ argumentParser.requiredOption("--organizationName", "Organization name to use for the audit.");
34
+ argumentParser.requiredOption("--secret", "Secret to use for api calls, it should be a GPAT.");
35
+ }
36
+ async getGitHubRepositoryContributors(gitHubApiClient, repositories, batchSize) {
37
+ const contributorsArray = [];
38
+ for (let i = 0; i < repositories.length; i += batchSize) {
39
+ const batch = repositories.slice(i, i + batchSize);
40
+ const results = await Promise.all(batch.map((repo) => gitHubApiClient.getGitHubRepositoryContributors(repo)));
41
+ contributorsArray.push(...results);
42
+ }
43
+ return utilities_1.DataMappingUtilities.mergeContributors(contributorsArray);
44
+ }
45
+ }
46
+ exports.default = GitHubContributorAuditProvider;
@@ -0,0 +1,11 @@
1
+ export declare const SOOS_GITHUB_CONTRIBUTOR_AUDIT_CONSTANTS: {
2
+ Urls: {
3
+ API: {
4
+ Base: string;
5
+ };
6
+ Docs: {
7
+ PAT: string;
8
+ };
9
+ };
10
+ RequestBatchSize: number;
11
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOOS_GITHUB_CONTRIBUTOR_AUDIT_CONSTANTS = void 0;
4
+ exports.SOOS_GITHUB_CONTRIBUTOR_AUDIT_CONSTANTS = {
5
+ Urls: {
6
+ API: {
7
+ Base: "https://api.github.com/",
8
+ },
9
+ Docs: {
10
+ PAT: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens",
11
+ },
12
+ },
13
+ RequestBatchSize: 10,
14
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./GitHubApiClient";
2
+ export * from "./GitHubContributorAuditProvider";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./GitHubApiClient"), exports);
5
+ tslib_1.__exportStar(require("./GitHubContributorAuditProvider"), exports);
@@ -0,0 +1,2 @@
1
+ export * from "./GitHub";
2
+ export * from "./BitbucketCloud";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./GitHub"), exports);
5
+ tslib_1.__exportStar(require("./BitbucketCloud"), exports);
@@ -0,0 +1,10 @@
1
+ import { IContributorAuditRepositories, IContributorAuditRepository } from "@soos-io/api-client/dist/api/SOOSHooksApiClient";
2
+ declare const ParamUtilities: {
3
+ getAsString(params: Record<string, string | number>, key: string): string;
4
+ getAsNumber(params: Record<string, string | number>, key: string): number;
5
+ };
6
+ declare const DataMappingUtilities: {
7
+ updateContributors(acc: IContributorAuditRepositories[], repo: IContributorAuditRepository, username: string, commitDate: string): IContributorAuditRepositories[];
8
+ mergeContributors(contributorsArray: IContributorAuditRepositories[][]): IContributorAuditRepositories[];
9
+ };
10
+ export { ParamUtilities, DataMappingUtilities };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataMappingUtilities = exports.ParamUtilities = void 0;
4
+ const ParamUtilities = {
5
+ getAsString(params, key) {
6
+ const value = params[key];
7
+ if (typeof value !== "string") {
8
+ throw new Error(`Expected string for parameter '${key}', got ${typeof value}`);
9
+ }
10
+ return value;
11
+ },
12
+ getAsNumber(params, key) {
13
+ const value = params[key];
14
+ if (typeof value !== "number") {
15
+ throw new Error(`Expected number for parameter '${key}', got ${typeof value}`);
16
+ }
17
+ return value;
18
+ },
19
+ };
20
+ exports.ParamUtilities = ParamUtilities;
21
+ const DataMappingUtilities = {
22
+ updateContributors(acc, repo, username, commitDate) {
23
+ const existingContributor = acc.find((contributor) => contributor.username === username);
24
+ if (!existingContributor) {
25
+ acc.push({ username, repositories: [repo] });
26
+ }
27
+ else {
28
+ const existingRepository = existingContributor.repositories.find((r) => r.id === repo.id);
29
+ if (!existingRepository) {
30
+ existingContributor.repositories.push(repo);
31
+ }
32
+ else if (new Date(existingRepository.lastCommit) < new Date(commitDate)) {
33
+ existingRepository.lastCommit = commitDate;
34
+ }
35
+ }
36
+ return acc;
37
+ },
38
+ mergeContributors(contributorsArray) {
39
+ const flattenedContributors = contributorsArray.flat();
40
+ const mergedContributors = new Map();
41
+ flattenedContributors.forEach((contributor) => {
42
+ const existingContributor = mergedContributors.get(contributor.username);
43
+ if (existingContributor) {
44
+ contributor.repositories.forEach((repository) => {
45
+ if (!existingContributor.repositories.find((r) => r.id === repository.id)) {
46
+ existingContributor.repositories.push(repository);
47
+ }
48
+ });
49
+ }
50
+ else {
51
+ mergedContributors.set(contributor.username, contributor);
52
+ }
53
+ });
54
+ return Array.from(mergedContributors.values());
55
+ },
56
+ };
57
+ exports.DataMappingUtilities = DataMappingUtilities;
@@ -0,0 +1,2 @@
1
+ export * from "./ContributorAuditService";
2
+ export * from "./ContributorAuditArgumentParser";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./ContributorAuditService"), exports);
5
+ tslib_1.__exportStar(require("./ContributorAuditArgumentParser"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soos-io/soos-scm-audit",
3
- "version": "0.1.3",
3
+ "version": "0.2.0-pre.1",
4
4
  "description": "SOOS SCM Audit CLI",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -31,13 +31,12 @@
31
31
  },
32
32
  "homepage": "https://github.com/soos-io/soos-scm-audit#readme",
33
33
  "dependencies": {
34
- "@soos-io/api-client": "1.2.2",
35
- "argparse": "^2.0.1",
34
+ "@soos-io/api-client": "1.3.0-pre.2",
35
+ "commander": "^13.1.0",
36
36
  "tslib": "^2.6.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@eslint/js": "^9.21.0",
40
- "@types/argparse": "^2.0.16",
41
40
  "@types/node": "^20.11.30",
42
41
  "eslint": "^9.21.0",
43
42
  "globals": "^16.0.0",