@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
JavaScript
;
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;