UNPKG

@enterprise-cmcs/macpro-security-hub-sync

Version:

NPM module to create Jira issues for all findings in Security Hub for the current AWS account..

363 lines (362 loc) 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Jira = void 0; const jira_client_1 = __importDefault(require("jira-client")); const dotenv = __importStar(require("dotenv")); const axios_1 = __importDefault(require("axios")); dotenv.config(); class Jira { jira; jiraClosedStatuses; constructor() { Jira.checkEnvVars(); this.jiraClosedStatuses = process.env.JIRA_CLOSED_STATUSES ? process.env.JIRA_CLOSED_STATUSES.split(",").map((status) => status.trim()) : ["Done"]; const jiraParams = { protocol: "https", host: process.env.JIRA_HOST, port: "443", username: process.env.JIRA_USERNAME, apiVersion: "2", strictSSL: true, }; if (process.env.JIRA_HOST?.includes("jiraent")) { jiraParams.bearer = process.env.JIRA_TOKEN; } else { jiraParams.password = process.env.JIRA_TOKEN; } this.jira = new jira_client_1.default(jiraParams); } async doesUserExist(accountId) { try { const user = await this.jira.getUser(accountId, "groups"); // User exists return true; } catch (err) { if (err.statusCode === 404) { // User does not exist return false; } else { try { const user = await this.jira.searchUsers({ username: accountId, query: "", }); return true; } catch (e) { // Handle other errors if needed console.error(err); return false; } // Handle other errors if needed console.error(err); return false; } } } async removeCurrentUserAsWatcher(issueKey) { try { const currentUser = await this.jira.getCurrentUser(); // Remove the current user as a watcher const axiosHeader = { Authorization: "", }; if (process.env.JIRA_HOST?.includes("jiraent")) { axiosHeader["Authorization"] = `Bearer ${process.env.JIRA_TOKEN}`; await (0, axios_1.default)({ method: "DELETE", url: `https://${process.env.JIRA_HOST}/rest/api/2/issue/${issueKey}/watchers`, headers: axiosHeader, params: { username: currentUser.name, }, }); } else { axiosHeader["Authorization"] = `Basic ${Buffer.from(`${process.env.JIRA_USERNAME}:${process.env.JIRA_TOKEN}`).toString("base64")}`; await (0, axios_1.default)({ method: "DELETE", url: `https://${process.env.JIRA_HOST}/rest/api/3/issue/${issueKey}/watchers`, headers: axiosHeader, params: { accountId: currentUser.accountId, }, }); } } catch (err) { console.error("Error creating issue or removing watcher:", err); } } static checkEnvVars() { const requiredEnvVars = [ "JIRA_HOST", "JIRA_USERNAME", "JIRA_TOKEN", "JIRA_PROJECT", ]; const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); if (missingEnvVars.length) { throw new Error(`Missing required environment variables: ${missingEnvVars.join(", ")}`); } } static formatLabelQuery(label) { return `labels = '${label}'`; } createSearchLabels(identifyingLabels, config) { const labels = []; const fields = ["accountId", "region", "identify"]; const values = [...identifyingLabels, "security-hub"]; config.forEach(({ labelField: field, labelDelimiter: delim, labelPrefix: prefix }) => { const delimiter = delim ?? ""; const labelPrefix = prefix ?? ""; if (fields.includes(field)) { const index = fields.indexOf(field); if (index >= 0) { labels.push(`${labelPrefix}${delimiter}${values[index] ?.trim() .replace(/ /g, "")}`); } } }); return labels; } async getAllSecurityHubIssuesInJiraProject(identifyingLabels) { const labelQueries = [...identifyingLabels, "security-hub"] .map((label) => Jira.formatLabelQuery(label)) .join(" AND "); let finalLabelQuery = labelQueries; if (process.env.LABELS_CONFIG) { const config = JSON.parse(process.env.LABELS_CONFIG); const configLabels = this.createSearchLabels(identifyingLabels, config); const searchQuery = configLabels .map((label) => Jira.formatLabelQuery(label)) .join(" AND "); if (searchQuery) { finalLabelQuery = `(${finalLabelQuery}) OR (${searchQuery})`; } } const projectQuery = `project = '${process.env.JIRA_PROJECT}'`; const statusQuery = `status not in ('${this.jiraClosedStatuses.join("','")}')`; const fullQuery = [finalLabelQuery, projectQuery, statusQuery].join(" AND "); // We want to do everything possible to prevent matching tickets that we shouldn't if (!fullQuery.includes(Jira.formatLabelQuery("security-hub"))) { throw new Error("ERROR: Your query does not include the 'security-hub' label, and is too broad. Refusing to continue"); } if (!fullQuery.match(Jira.formatLabelQuery("[0-9]{12}"))) { throw new Error("ERROR: Your query does not include an AWS Account ID as a label, and is too broad. Refusing to continue"); } let totalIssuesReceived = 0; let allIssues = []; let results; const searchOptions = {}; console.log(fullQuery, searchOptions); try { do { results = await this.jira.searchJira(fullQuery, searchOptions); allIssues = allIssues.concat(results.issues); totalIssuesReceived += results.issues.length; searchOptions.startAt = totalIssuesReceived; } while (totalIssuesReceived < results.total); } catch (e) { throw new Error(`Error getting Security Hub issues from Jira: ${e.message}`); } return allIssues; } async getPriorityIdsInDescendingOrder() { try { const priorities = await this.jira.listPriorities(); // Get priority IDs in descending order const descendingPriorityIds = priorities.map((priority) => priority.id); return descendingPriorityIds; } catch (err) { console.error(err); return []; } } async createNewIssue(issue) { try { const assignee = process.env.ASSIGNEE ?? ""; if (assignee) { const isAssignee = await this.doesUserExist(assignee); if (isAssignee) { if (process.env.JIRA_HOST?.includes("jiraent")) { issue.fields.assignee = { name: assignee }; } else { issue.fields.assignee = { accountId: assignee }; } } } issue.fields.project = { key: process.env.JIRA_PROJECT }; const newIssue = await this.jira.addNewIssue(issue); newIssue["webUrl"] = `https://${process.env.JIRA_HOST}/browse/${newIssue.key}`; await this.removeCurrentUserAsWatcher(newIssue.key); return newIssue; } catch (e) { throw new Error(`Error creating Jira issue: ${e.message}`); } } async linkIssues(newIssueKey, issueID, linkType = "Relates", linkDirection = "inward") { const linkData = { type: { name: linkType }, inwardIssue: { key: newIssueKey }, outwardIssue: { key: issueID }, }; if (linkDirection == "outward") { const temp = linkData.inwardIssue.key; linkData.inwardIssue.key = linkData.outwardIssue.key; linkData.outwardIssue.key = temp; } try { await this.jira.issueLink(linkData); console.log(`Successfully linked issue ${newIssueKey} with ${issueID}`); } catch (error) { console.error("Error linking issues:", error); } } async updateIssueTitleById(issueId, updatedIssue) { try { const response = await this.jira.updateIssue(issueId, updatedIssue); console.log("Issue title updated successfully:", response); } catch (error) { console.error("Error updating issue title:", error); } } async addCommentToIssueById(issueId, comment) { try { const response = await this.jira.addComment(issueId, comment); } catch (error) { console.error("Error adding comment:", error); } } async findPathToClosure(transitions, currentStatus) { const visited = new Set(); const queue = [ { path: [], status: currentStatus }, ]; while (queue.length > 0) { const { path, status } = queue.shift(); visited.add(status); const possibleTransitions = transitions.filter((transition) => transition.from.name === status); for (const transition of possibleTransitions) { const newPath = [...path, transition.id]; const newStatus = transition.to.name; if (newStatus.toLowerCase().includes("close") || newStatus.toLowerCase().includes("done")) { return newPath; // Found a path to closure } if (!visited.has(newStatus)) { queue.push({ path: newPath, status: newStatus }); } } } return []; // No valid path to closure found } async completeWorkflow(issueKey) { const opposedStatuses = ["canceled", "backout", "rejected"]; try { const issue = await this.jira.findIssue(issueKey); const processedTransitions = []; do { const availableTransitions = await this.jira.listTransitions(issueKey); if (availableTransitions.transitions.length > 0) { const targetTransitions = availableTransitions.transitions.filter((transition) => !opposedStatuses.includes(transition.name.toLowerCase()) && !processedTransitions.includes(transition.name.toLowerCase())); if (targetTransitions.length <= 0) { if (!processedTransitions.length) { throw new Error("Unsupported workflow; no transition available"); } const lastStatus = processedTransitions[processedTransitions.length - 1].toLowerCase(); const doneStatuses = ["done", "closed", "close", "complete"]; if (!doneStatuses.includes(lastStatus)) { throw new Error("Unsupported Workflow: does not contain any of " + doneStatuses.join(",") + "statuses"); } break; } const transitionId = targetTransitions[0].id; processedTransitions.push(targetTransitions[0].name.toLowerCase()); await this.jira.transitionIssue(issueKey, { transition: { id: transitionId }, }); console.log(`Transitioned issue ${issueKey} to the next stage: ${targetTransitions[0].name}`); } else { break; } } while (true); } catch (e) { console.log("Error completing the workflow ", e); } } async closeIssue(issueKey) { if (!issueKey) return; try { const transitions = await this.jira.listTransitions(issueKey); const doneTransition = transitions.transitions.find((t) => t.name === "Done"); if (!doneTransition) { this.completeWorkflow(issueKey); return; } await this.jira.transitionIssue(issueKey, { transition: { id: doneTransition.id }, }); } catch (e) { throw new Error(`Error closing issue ${issueKey}: ${e.message}`); } } } exports.Jira = Jira;