diff --git a/security_dashboard/Code.gs b/security_dashboard/Code.gs
new file mode 100644
index 0000000..9403fd3
--- /dev/null
+++ b/security_dashboard/Code.gs
@@ -0,0 +1,458 @@
+
+/**
+ * @OnlyCurrentDoc
+ *
+ * The above comment directs App Script to limit the scope of authorization
+ * to only the current spreadsheet.
+ */
+
+/**
+ * Initial setup: Add the following sheet properties with the following values
+ * SHEET_ID:
+ * ALL_VULNS_SHEET_NAME: AllVulnerabilities_Normalized
+ * AUDIT_SHEET_NAME: Audit_Import
+ * GH_CODESCANNING_SHEET_NAME: GHAS_CodeScanning_Import
+ * GH_DEPENDABOT_SHEET_NAME: GHAS_Dependabot_Import
+ * GH_SECRETSSCANNING_SHEET_NAME: GHAS_Secrets_Import
+ * GITHUB_ENTERPRISE_URL: https://github.com/enterprises/stellar-development-foundation
+ * GITHUB_ORG: stellar
+ * JIRA_BUGBOUNTY_SHEET_NAME: Jira_BugBounty_Import
+ * JIRA_EMAIL:
+ * JIRA_URL: https://stellarorg.atlassian.net
+ *
+ */
+
+
+
+//Data Normalization Constants
+const SEV_CRITICAL = 'Critical';
+const SEV_HIGH = 'High';
+const SEV_MODERATE = 'Moderate';
+const SEV_LOW = 'Low';
+
+/**
+ * Creates a custom menu in the spreadsheet to run the importer.
+ */
+function onOpen() {
+ SpreadsheetApp.getUi()
+ .createMenu('Vulnerability Imports')
+ .addItem('Import All Vulnerability Sources', 'importVulnerabilitiesToSheet')
+ .addItem('Import GH Dependabot Vulnerabilities', 'importDependabotVulnerabilitiesToSheet')
+ .addItem('Import GH Secrets Scanning Vulnerabilities', 'importSecretScanningVulnerabilitiesToSheet')
+ .addItem('Import GH CodeScanning Vulnerabilities', 'importCodeScanningVulnerabilitiesToSheet')
+ .addItem('Import Jira Bug Bounty Vulnerabilities', 'importBugBountyVulnerabilitiesToSheet')
+ .addItem('Synch all sources to Normalized Vulnerability Tab','dataSynch_AllVulnerabilities')
+ .addToUi();
+}
+
+/**
+ * Fetches the script properties set by the user.
+ * @returns {object} The script properties.
+ */
+function getScriptProperties() {
+ const properties = PropertiesService.getScriptProperties();
+ const userProperties = PropertiesService.getUserProperties();
+ const githubEnterpriseUrl = properties.getProperty('GITHUB_ENTERPRISE_URL');
+ const githubOrg = properties.getProperty('GITHUB_ORG');
+ let githubToken = userProperties.getProperty('GITHUB_TOKEN');
+ if (githubToken === null) {
+ const ui = SpreadsheetApp.getUi();
+ const result = ui.prompt(
+ 'Setup Required',
+ `The GitHub token is not set. Please enter your classic API token to continue.`,
+ ui.ButtonSet.OK_CANCEL);
+
+ // Check if the user clicked 'OK' and provided a value.
+ if (result.getSelectedButton() == ui.Button.OK && result.getResponseText() !== '') {
+ // Get the user's input.
+ githubToken = result.getResponseText();
+
+ // 6. Store the new value.
+ userProperties.setProperty('GITHUB_TOKEN', githubToken);
+ Logger.log(`Property GITHUB_TOKEN has been set and stored.`);
+ }
+ }
+ const sheetId = properties.getProperty('SHEET_ID');
+ const dependabotSheetName = properties.getProperty('GH_DEPENDABOT_SHEET_NAME');
+ const secretsScanningSheetName = properties.getProperty('GH_SECRETSSCANNING_SHEET_NAME');
+ const codeScanningSheetName = properties.getProperty('GH_CODESCANNING_SHEET_NAME');
+
+ if (!githubEnterpriseUrl || !githubOrg || !githubToken || !sheetId || !dependabotSheetName || !secretsScanningSheetName || !codeScanningSheetName) {
+ throw new Error("One or more script properties are not set. Please configure them in Project Settings.");
+ }
+
+ return { githubEnterpriseUrl, githubOrg, githubToken, sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName };
+}
+
+/**
+ * Fetches all repositories for the configured GitHub organization.
+ * @param {string} apiUrl - The GitHub GraphQL API endpoint.
+ * @param {string} orgName - The name of the organization.
+ * @param {string} token - The GitHub Personal Access Token.
+ * @returns {Array} A list of repository names.
+ */
+//
+function getOrgRepositories(apiUrl, orgName, token) {
+ const query = `
+ query($org: String!, $cursor: String) {
+ organization(login: $org) {
+ repositories(first: 100, isArchived: false, after: $cursor) {
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ nodes {
+ name
+ isArchived
+ }
+ }
+ }
+ }
+ `;
+
+ let repositories = [];
+ let hasNextPage = true;
+ let cursor = null;
+ const ui = SpreadsheetApp.getUi();
+
+ while (hasNextPage) {
+ const variables = { org: orgName, cursor: cursor };
+ const payload = JSON.stringify({ query, variables });
+ const options = {
+ 'method': 'POST',
+ 'contentType': 'application/json',
+ 'headers': {
+ 'Authorization': `Bearer ${token}`
+ },
+ 'payload': payload,
+ 'muteHttpExceptions': true
+ };
+ //ui.alert("pre-fetch");
+ //apiUrl="https://api.github.com/graphql";
+ const response = UrlFetchApp.fetch(apiUrl, options);
+ //ui.alert(response.getContentText());
+ //Bug catching---
+ const responseCode = response.getResponseCode();
+ const responseBody = response.getContentText();
+
+ // Check for non-200 HTTP status codes
+ if (responseCode !== 200) {
+ Logger.log(`GitHub API request failed in getOrgRepositories. Status: ${responseCode}. Body: ${responseBody}`);
+ // Try to parse the error for a better message
+ let detailedError = `Status code ${responseCode}.`;
+ try {
+ const errorResult = JSON.parse(responseBody);
+ if (errorResult.message) {
+ detailedError += ` Message: ${errorResult.message}`;
+ }
+ } catch (e) {
+ // Body is not JSON, just show the raw body
+ detailedError += ` Response: ${responseBody}`;
+ }
+ const errorMessage = `GitHub API request failed. ${detailedError} Check Logs and Script Properties.`;
+ ui.alert('GitHub API Error', errorMessage, ui.ButtonSet.OK);
+ throw new Error(errorMessage);
+ }
+
+ const result = JSON.parse(response.getContentText());
+
+ if (result.errors) {
+ ui.alert('GitHub API Error', result.errors[0].message, ui.ButtonSet.OK);
+ throw new Error(result.errors[0].message);
+ }
+
+ //End of bug catching---
+ const data = result.data.organization.repositories;
+ repositories = repositories.concat(data.nodes.map(repo => repo.name));
+ hasNextPage = data.pageInfo.hasNextPage;
+ cursor = data.pageInfo.endCursor;
+ }
+
+ return repositories;
+}
+
+
+/**
+ * Fetches vulnerability alerts for a specific repository.
+ * @param {string} apiUrl - The GitHub GraphQL API endpoint.
+ * @param {string} orgName - The name of the organization.
+ * @param {string} repoName - The name of the repository.
+ * @param {string} token - The GitHub Personal Access Token.
+ * @returns {Array} A list of vulnerability alert objects.
+ */
+function getVulnerabilitiesForRepo(apiUrl, orgName, repoName, token) {
+ const query = `
+ query($org: String!, $repo: String!, $cursor: String) {
+ repository(owner: $org, name: $repo) {
+ vulnerabilityAlerts(first: 100, after: $cursor) {
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ nodes {
+ createdAt
+ autoDismissedAt
+ dismissedAt
+ dismissComment
+ dismisser {
+ name
+ }
+ dismissReason
+ fixedAt
+ state
+ number
+ securityVulnerability {
+ package {
+ name
+ }
+ severity
+ advisory {
+ summary
+ ghsaId
+ }
+ }
+ vulnerableManifestFilename
+ vulnerableManifestPath
+ vulnerableRequirements
+ }
+ }
+ }
+ }
+ `;
+
+ let vulnerabilities = [];
+ let hasNextPage = true;
+ let cursor = null;
+ const ui = SpreadsheetApp.getUi();
+
+ while (hasNextPage) {
+ const variables = { org: orgName, repo: repoName, cursor: cursor };
+ const payload = JSON.stringify({ query, variables });
+
+ const options = {
+ 'method': 'post',
+ 'contentType': 'application/json',
+ 'headers': {
+ 'Authorization': `Bearer ${token}`
+ },
+ 'payload': payload,
+ 'muteHttpExceptions': true
+ };
+
+ const response = UrlFetchApp.fetch(apiUrl, options);
+ const result = JSON.parse(response.getContentText());
+
+ if (result.errors) {
+ // It's possible the repo has no vulnerability alerts enabled, so we log instead of stopping.
+ console.log(`Could not fetch vulnerabilities for ${repoName}: ${result.errors[0].message}`);
+ return [];
+ }
+
+ if (!result.data.repository || !result.data.repository.vulnerabilityAlerts) {
+ console.log(`No vulnerability alerts found or feature not enabled for ${repoName}.`);
+ return [];
+ }
+
+ const data = result.data.repository.vulnerabilityAlerts;
+ vulnerabilities = vulnerabilities.concat(data.nodes);
+ hasNextPage = data.pageInfo.hasNextPage;
+ cursor = data.pageInfo.endCursor;
+ }
+
+ return vulnerabilities;
+}
+
+
+/**
+ * Main function to import vulnerabilities into the Google Sheet.
+ */
+function importVulnerabilitiesToSheet() {
+ importDependabotVulnerabilitiesToSheet();
+ importSecretScanningVulnerabilitiesToSheet();
+ importCodeScanningVulnerabilitiesToSheet();
+ importBugBountyVulnerabilitiesToSheet();
+ dataSynch_AllVulnerabilities();
+
+}
+
+const COL_DP_TEAM_NAME = 1;
+const COL_DP_PROJECT_NAME = 2;
+const COL_DP_REPOSITORY = 3;
+const COL_DP_PACKAGE = 4;
+const COL_DP_SEVERITY = 5;
+const COL_DP_SUMMARY = 6;
+const COL_DP_CREATED_AT = 7;
+const COL_DP_AUTO_DISMISSED_AT = 8;
+const COL_DP_DISMISSED_AT = 9;
+const COL_DP_DISMISS_COMMENT = 10;
+const COL_DP_DISMISSER_NAME = 11;
+const COL_DP_DISMISS_REASON = 12;
+const COL_DP_FIXED_AT = 13;
+const COL_DP_STATUS = 14;
+const COL_DP_GHSA_ID = 15;
+const COL_DP_REPO_LINK = 16;
+const COL_DP_VULNERABLE_FILENAME = 17;
+const COL_DP_VULNERABLE_FILEPATH = 18;
+const COL_DP_VULNERABLE_REQUIREMENTS = 19;
+const COL_DP_DAYS_OPENED = 20;
+const COL_DP_REMEDIATION_DEADLINE = 21;
+const COL_DP_SOURCE = 22;
+
+function importDependabotVulnerabilitiesToSheet() {
+ const ui = SpreadsheetApp.getUi();
+ try {
+ const { githubEnterpriseUrl, githubOrg, githubToken, sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName } = getScriptProperties();
+ //const apiUrl = `${githubEnterpriseUrl}/api/graphql`;
+ const apiUrl = `https://api.github.com/graphql`;
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Starting vulnerability import...
Fetching repositories...
').setTitle('Import Progress'));
+
+ const repositories = getOrgRepositories(apiUrl, githubOrg, githubToken);
+ //ui.showSidebar(HtmlService.createHtmlOutput('here with' + repositories.length + ' repos right after finding them!'));
+ if (repositories.length === 0) {
+ ui.alert('No repositories found in the organization.');
+ return;
+ }
+
+ let allVulnerabilities = [];
+ //ui.alert('here with' + repositories.length + ' repos!');
+ //ui.showSidebar(HtmlService.createHtmlOutput('here with' + repositories.length + ' repos!'));
+ //var progressHtml = '';
+ for (let i = 0; i < repositories.length; i++) {
+ //for (let i = 0; i < 15; i++) {
+ const repoName = repositories[i];
+ //const progressHtml = `Processing repo ${i + 1} of ${repositories.length}: ${repoName}
`;
+ if((i+1)%5 == 0) { //Only update status every 5 repos so as not to overload updates
+ ui.showSidebar(HtmlService.createHtmlOutput(`Processing repo ${i + 1} of ${repositories.length}: ${repoName}
`).setTitle('Import Progress'));
+ }
+
+ const vulnerabilities = getVulnerabilitiesForRepo(apiUrl, githubOrg, repoName, githubToken);
+ vulnerabilities.forEach(vuln => {
+ allVulnerabilities.push([
+ "PlaceholderTeamName",
+ "PlaceholderProjectName",
+ repoName,
+ vuln.securityVulnerability.package.name,
+ vuln.securityVulnerability.severity,
+ vuln.securityVulnerability.advisory.summary,
+ new Date(vuln.createdAt),
+ new Date(vuln.autoDismissedAt),
+ new Date(vuln.dismissedAt),
+ vuln.dismissComment,
+ (vuln.dismisser==null)?"":vuln.dismisser.name,
+ vuln.dismissReason,
+ new Date (vuln.fixedAt),
+ vuln.state,
+ vuln.securityVulnerability.advisory.ghsaId,
+ "https://github.com/"+githubOrg+"/"+repoName+"/security/dependabot/"+vuln.number,
+ vuln.vulnerableManifestFilename,
+ vuln.vulnerableManifestPath,
+ "'"+vuln.vulnerableRequirements,
+ "Placeholder - Days Open",
+ "Placeholder - Remediation Deadline",
+ "GitHub Vulnerabilities (Dependabot)"
+ ]);
+ });
+ }
+
+ const spreadsheet = SpreadsheetApp.openById(sheetId);
+ const sheet = spreadsheet.getSheetByName(dependabotSheetName);
+
+ if (!sheet) {
+ throw new Error(`Sheet with name "${dependabotSheetName}" not found.`);
+ }
+
+ // Clear existing data and set headers
+ sheet.clear();
+ const headers = ['Team Name', 'Project Name','Repository', 'Package', 'Severity', 'Summary', 'Created At', 'Auto Dismissed At', 'Dismissed At', 'Dismiss Comment', 'Dismisser Name', 'Dismiss Reason', 'FixedAt', 'Status', 'GHSA ID', 'Repo Link', 'Vulnerable Filename', 'Vulnerable Filepath', 'Vulnerable Requirements', 'Days Opened', 'Remediation Deadline', 'Source'];
+ sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
+
+
+
+
+
+ if (allVulnerabilities.length > 0) {
+ sheet.getRange(2, 1, allVulnerabilities.length, headers.length).setValues(allVulnerabilities);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Team <-> Repo mappings!
').setTitle('Import Progress'));
+ //Set Team Repo Mapping
+ sheet.getRange(2,COL_DP_TEAM_NAME,allVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1)),\"\",xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1))"); //Set reference formulas for calculated data - Project name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Project <-> Repo mappings!
').setTitle('Import Progress'));
+ //Set Project Repo Mapping
+ sheet.getRange(2,COL_DP_PROJECT_NAME,allVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2)),\"\",xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2))"); //Set reference formulas for calculated data - Project name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating days open calculations!
').setTitle('Import Progress'));
+ //Set Days Opened calculation - =If(Not(isBlank(H2)),datedif(F2,H2,"D"),If(Not(isBlank(L2)),datedif(F2,L2,"D"),If(Not(isBlank(G2)),datedif(F2,G2,"D"), datedif(F2,today(),"D"))))
+ sheet.getRange(2,COL_DP_DAYS_OPENED,allVulnerabilities.length,1).setFormulaR1C1('=If(Not(isBlank(R[0]C[-11])),datedif(R[0]C[-13],R[0]C[-11],"D"),If(Not(isBlank(R[0]C[-7])),datedif(R[0]C[-13],R[0]C[-7],"D"),If(Not(isBlank(R[0]C[-12])),datedif(R[0]C[-13],R[0]C[-12],"D"),datedif(R[0]C[-13],today(),"D"))))');
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating remediation deadline calculations!
').setTitle('Import Progress'));
+ //Set Remediation Deadline Calculation - =F2+XLOOKUP(D2,RemediationPolicyTimelines!$A$1:$A$4,RemediationPolicyTimelines!$B$1:$B$4)
+ sheet.getRange(2,COL_DP_REMEDIATION_DEADLINE,allVulnerabilities.length,1).setFormulaR1C1('=R[0]C[-14]+XLOOKUP(R[0]C[-16],RemediationPolicyTimelines!R1C1:R4C1,RemediationPolicyTimelines!R1C2:R4C2)');
+ }
+
+ //DATA CLEANUP
+ var startRow = 2;
+ var lastRow = allVulnerabilities.length
+ //var valueToDelete = new Date('12/31/1969 19:00:00');
+ //var valueToDelete = new Date('');
+ var valueToDelete = new Date(null);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up AutoDismissedAt empty dates!
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Auto Dismissed At' Column
+ var columnNumber = COL_DP_AUTO_DISMISSED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up DismissedAt empty dates!
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Dismissed At' Column
+ columnNumber = COL_DP_DISMISSED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ //ui.alert('SS value'+ values[i][0] + ' :: Compares to ' + valueToDelete);
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent(); //i+startRow since we are offsetting from the start row
+ }
+ }
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up FixedAt empty dates!
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Fixed At' Column
+ columnNumber = COL_DP_FIXED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+
+
+
+ //Set Custom Formulas for this import
+
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Import complete!
').setTitle('Dependabot Import Progress'));
+ ui.alert('Success', 'Dependabot Vulnerability data has been imported successfully.', ui.ButtonSet.OK);
+
+ } catch (e) {
+ ui.alert('Error', e.message, ui.ButtonSet.OK);
+ Logger.log(e.toString());
+ }
+}
+
+function sheetName() {
+ return SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName();
+}
diff --git a/security_dashboard/CodeScanningImport.gs b/security_dashboard/CodeScanningImport.gs
new file mode 100644
index 0000000..b6df480
--- /dev/null
+++ b/security_dashboard/CodeScanningImport.gs
@@ -0,0 +1,193 @@
+/**
+ * Fetches vulnerability alerts for a specific repository.
+ * @param {string} secretsAPIUrl - The GitHub GraphQL API endpoint.
+ * @param {string} orgName - The name of the organization.
+ * @param {string} repoName - The name of the repository.
+ * @param {string} token - The GitHub Personal Access Token.
+ * @returns {Array} A list of vulnerability alert objects.
+ */
+function getCodeScanningVulnerabilitiesForOrg(orgName, token) {
+ let codeScanningAPIUrl = "https://api.github.com/orgs/"+orgName+"/code-scanning/alerts";
+ let vulnerabilities = [];
+ let hasNextPage = true;
+ let cursor = null;
+ const ui = SpreadsheetApp.getUi();
+
+ const options = {
+ 'method': 'get',
+ 'contentType': 'application/json',
+ 'headers': {
+ 'Authorization': `Bearer ${token}`,
+ 'Accept': 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+
+ 'muteHttpExceptions': true
+ };
+
+ const response = UrlFetchApp.fetch(codeScanningAPIUrl, options);
+ const responseCode = response.getResponseCode();
+ const responseBody = response.getContentText();
+ Logger.log(responseBody);
+
+ if (responseCode !== 200) {
+ if (responseCode === 404) {
+ throw new Error(`Organization '${orgName}' not found or Code Scanning is not enabled.`);
+ }
+ if (responseCode === 401) {
+ throw new Error("Authentication failed. Check if your GITHUB_TOKEN is correct and has the right scopes.");
+ }
+ // Log the full error for debugging
+ Logger.log(responseBody);
+ throw new Error(`GitHub API request failed with code ${responseCode}.`);
+ }
+
+ const codeScanningAlerts = JSON.parse(responseBody);
+ return codeScanningAlerts;
+
+}
+
+const COL_CS_TEAM_NAME = 1;
+const COL_CS_PROJECT_NAME = 2;
+const COL_CS_REPO_NAME = 3;
+const COL_CS_SEVERITY = 4;
+const COL_CS_SUMMARY = 5;
+const COL_CS_CREATED_AT = 6;
+const COL_CS_DISMISSED_AT = 7;
+const COL_CS_DISMISS_COMMENT = 8;
+const COL_CS_DISMISSER_NAME = 9;
+const COL_CS_DISMISS_REASON = 10;
+const COL_CS_FIXED_AT = 11;
+const COL_CS_STATUS = 12;
+const COL_CS_ID_NUMBER = 13;
+const COL_CS_REPO_LINK = 14;
+const COL_CS_DAYS_OPENED = 15;
+const COL_CS_REMEDIATION_DEADLINE = 16;
+const COL_CS_SOURCE = 17;
+
+function importCodeScanningVulnerabilitiesToSheet() {
+ const ui = SpreadsheetApp.getUi();
+ try{
+ const { githubEnterpriseUrl, githubOrg, githubToken, sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName } = getScriptProperties();
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Starting vulnerability import...
Fetching code scanning results...
').setTitle('Import Progress'));
+
+ let allCodeScanningVulnerabilities = [];
+ const vulnerabilities = getCodeScanningVulnerabilitiesForOrg(githubOrg, githubToken);
+ vulnerabilities.forEach(vuln => {
+ allCodeScanningVulnerabilities.push([
+ 'Placeholder - TeamName',
+ 'Placeholder - ProjectName',
+ vuln.repository.name,
+ vuln.rule.security_severity_level,
+ vuln.rule.description,
+ new Date(vuln.created_at),
+ new Date(vuln.dismissed_at),
+ vuln.dismissed_comment,
+ (vuln.dismissed_by==null)?"":vuln.dismissed_by.login,
+ vuln.dismissed_reason,
+ new Date(vuln.fixed_at),
+ vuln.state,
+ vuln.number,
+ vuln.html_url,
+ "Placeholder - Days Opened",
+ "Placeholder - Remediation Deadline",
+ "Github Code Scanning"
+ ]);
+ });
+ const spreadsheet = SpreadsheetApp.openById(sheetId);
+ const sheet = spreadsheet.getSheetByName(codeScanningSheetName);
+ sheet.clear();
+ const headers = ['Team Name', 'Project Name', 'Repo Name', 'Severity', 'Summary', 'Created At', 'Dismissed At', 'Dismiss Comment', 'Dismisser Name', 'Dismiss Reason', 'Fixed At', 'Status', 'ID Number', 'Repo Link', 'Days Opened', 'Remediation Deadline', 'Source' ];
+
+ sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
+ sheet.getRange(2,1, allCodeScanningVulnerabilities.length, headers.length).setValues(allCodeScanningVulnerabilities);
+
+
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Team <-> Repo mapping...').setTitle('Import Progress'));
+ //Set Team Repo Mapping
+ sheet.getRange(2,COL_CS_TEAM_NAME,allCodeScanningVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1)),\"\",xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1))"); //Set reference formulas for calculated data - Team name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating Project <-> Repo mapping...').setTitle('Import Progress'));
+ //Set Project Repo Mapping
+ sheet.getRange(2,COL_CS_PROJECT_NAME,allCodeScanningVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2)),\"\",xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2))"); //Set reference formulas for calculated data - Project name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating days opened calculation...').setTitle('Import Progress'));
+ //Set Days Opened calculation - =If(Not(isBlank(DISMISSED_AT)),datedif(CREATED_AT,DISMISSED_AT,"D"),If(Not(isBlank(FIXED_AT)),datedif(CREATED_AT,FIXED_AT,"D"), datedif(CREATED_AT,today(),"D")))
+ sheet.getRange(2,COL_CS_DAYS_OPENED,allCodeScanningVulnerabilities.length,1).setFormulaR1C1('=If(Not(isBlank(R[0]C[-8])),datedif(R[0]C[-9],R[0]C[-8],"D"),If(Not(isBlank(R[0]C[-4])),datedif(R[0]C[-9],R[0]C[-4],"D"),datedif(R[0]C[-9],today(),"D")))');
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating remediation deadline calculation...').setTitle('Import Progress'));
+ //Set Remediation Deadline Calculation - =CREATED_AT+XLOOKUP(SEVERITY,RemediationPolicyTimelines!$A$1:$A$4,RemediationPolicyTimelines!$B$1:$B$4)
+ sheet.getRange(2,COL_CS_REMEDIATION_DEADLINE,allCodeScanningVulnerabilities.length,1).setFormulaR1C1('=R[0]C[-10]+XLOOKUP(R[0]C[-12],RemediationPolicyTimelines!R1C1:R4C1,RemediationPolicyTimelines!R1C2:R4C2)');
+
+ //DATA CLEANUP and Normalization
+ startRow = 2;
+ lastRow = allCodeScanningVulnerabilities.length
+ var valueToDelete = new Date(null);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Cleaning up Fixed At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Fixed At' Column
+ var columnNumber = COL_CS_FIXED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up Dismissed At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Dismissed At' Column
+ columnNumber = COL_CS_DISMISSED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ //Data Normalization
+ const CS_SEV_CRITICAL = 'critical';
+ const CS_SEV_HIGH = 'high';
+ const CS_SEV_MODERATE = 'medium';
+ const CS_SEV_LOW = 'low';
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Normalizing severity ratings...
').setTitle('Import Progress'));
+
+ columnNumber = COL_CS_SEVERITY;
+ var dataNormalizationRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var dataNormalizationValues = dataNormalizationRange.getValues();
+ //Iterate through the Values and update them to the global standard
+ for(var i = 0; iCode Scanning Import complete!
').setTitle('Import Progress'));
+ ui.alert('Success', 'Code Scanning data has been imported successfully.', ui.ButtonSet.OK);
+
+ } catch (e) {
+ ui.alert('Error', e.message, ui.ButtonSet.OK);
+ Logger.log(e.toString());
+ }
+}
diff --git a/security_dashboard/JiraImport.gs b/security_dashboard/JiraImport.gs
new file mode 100644
index 0000000..398bde0
--- /dev/null
+++ b/security_dashboard/JiraImport.gs
@@ -0,0 +1,373 @@
+/**
+ * Fetches the script properties set by the user.
+ * @returns {object} The script properties.
+ */
+function getJiraScriptProperties() {
+ const ui = SpreadsheetApp.getUi();
+ const properties = PropertiesService.getScriptProperties();
+ const userProperties = PropertiesService.getUserProperties();
+ const sheetID = properties.getProperty('SHEET_ID')
+ const JiraURL = properties.getProperty('JIRA_URL');
+ var JiraEmail = userProperties.getProperty('JIRA_EMAIL');
+ var JiraAPIToken = userProperties.getProperty('JIRA_API_TOKEN');
+
+ if (JiraEmail === null) {
+ const ui = SpreadsheetApp.getUi();
+ const result = ui.prompt(
+ 'Setup Required',
+ `The Jira Email for the API is not set. Please enter your Jira API email to continue.`,
+ ui.ButtonSet.OK_CANCEL);
+
+ // Check if the user clicked 'OK' and provided a value.
+ if (result.getSelectedButton() == ui.Button.OK && result.getResponseText() !== '') {
+ // Get the user's input.
+ JiraEmail = result.getResponseText();
+
+ // Store the new value.
+ userProperties.setProperty('JIRA_EMAIL', JiraEmail);
+ Logger.log(`Property JIRA_EMAIL has been set and stored.`);
+ }
+ }
+
+ if (JiraAPIToken === null) {
+ const ui = SpreadsheetApp.getUi();
+ const result = ui.prompt(
+ 'Setup Required',
+ `The Jira API token is not set. Please enter your Jira API token to continue.`,
+ ui.ButtonSet.OK_CANCEL);
+
+ // Check if the user clicked 'OK' and provided a value.
+ if (result.getSelectedButton() == ui.Button.OK && result.getResponseText() !== '') {
+ // Get the user's input.
+ JiraAPIToken = result.getResponseText();
+
+ // Store the new value.
+ userProperties.setProperty('JIRA_API_TOKEN', JiraAPIToken);
+ Logger.log(`Property JIRA_API_TOKEN has been set and stored.`);
+ }
+ }
+
+ const JiraBugBountySheetName = properties.getProperty('JIRA_BUGBOUNTY_SHEET_NAME');
+ if (!sheetID || !JiraURL || !JiraEmail || !JiraAPIToken || !JiraBugBountySheetName) {
+ throw new Error("One or more script properties are not set. Please configure them in Project Settings.")
+ }
+
+ return { sheetID, JiraURL, JiraEmail, JiraAPIToken, JiraBugBountySheetName};
+}
+
+
+function getJiraVulnerabilityImportsforOrg(JiraURL, JiraEmail, JiraAPIToken) {
+ /***
+ * Example URL for issue link https://your-org.atlassian.net/browse/issue-number
+ */
+
+ let JiraBase = JiraURL+"/rest/api/latest/search/jql?jql=";
+ let JiraJQL= "filter=10718";
+ let JiraFields = "*navigable";
+
+ let vulnerabilities = [];
+ let hasNextPage = true;
+ let bbNextPageToken = '';
+ let cursor = null;
+ const ui = SpreadsheetApp.getUi();
+
+ while (hasNextPage) {
+ //const variables = { org: orgName, repo: repoName, cursor: cursor };
+ //const payload = JSON.stringify({ query, variables });
+ const headers = {
+ "Authorization": `Basic ${Utilities.base64Encode(`${JiraEmail}:${JiraAPIToken}`)}`,
+ "Accept":"application/json"
+ }
+
+ const options = {
+ 'method': 'get',
+ 'contentType': 'application/json',
+ 'headers': headers,
+ 'muteHttpExceptions': true
+ };
+ JiraQueryURL = `${JiraBase}${encodeURIComponent(JiraJQL)}&fields=${JiraFields}&nextPageToken=${bbNextPageToken}`;
+ const response = UrlFetchApp.fetch(JiraQueryURL, options);
+ const responseCode = response.getResponseCode();
+ const responseBody = response.getContentText();
+ //Logger.log(responseBody);
+ //ui.alert(responseBody);
+
+
+ if (responseCode !== 200) {
+ // Log the error response from GitHub for debugging
+ Logger.log(`Error from Jira API: ${responseBody}`);
+ throw new Error(`Failed to fetch data. Jira API responded with code: ${responseCode}. Check Logs for details.`);
+ }
+
+ const bugBountyList = JSON.parse(responseBody);
+ const bugBountyIssues = bugBountyList.issues;
+ const vulnLength = vulnerabilities.push(bugBountyIssues);
+ //ui.alert(`${bugBountyIssues.length} issues added and a new total list of ${vulnLength}.`);
+
+ if (bugBountyList.isLast || bugBountyIssues.length === 0) {
+ //ui.alert(`${bugBountyList.nextPageToken}`)
+ Logger.log("No more issues found.");
+ hasNextPage = false;
+ continue;
+ } else {
+ bbNextPageToken=bugBountyList.nextPageToken;
+ }
+
+
+
+
+
+ }
+ return vulnerabilities;
+
+}
+
+const COL_BB_TEAM_NAME = 1;
+const COL_BB_PROJECT_NAME = 2;
+const COL_BB_SEVERITY = 3;
+const COL_BB_SUMMARY = 4;
+const COL_BB_CREATED_AT = 5;
+const COL_BB_TRIAGED_AT = 6;
+const COL_BB_VULNERABILITY_CONFIRMATION = 7;
+const COL_BB_PAYOUT_AMOUNT = 8;
+const COL_BB_FIXED_AT = 9;
+const COL_BB_STATUS = 10;
+const COL_BB_STATUS_EXTENDED = 11;
+const COL_BB_VULNERABILITY_ID = 12;
+const COL_BB_VULNERABILITY_LINK = 13;
+const COL_BB_PII_EXPOSURE = 14;
+const COL_BB_FINANCIAL_RISK = 15;
+const COL_BB_LABELS = 16;
+const COL_BB_DAYS_OPENED = 17;
+const COL_BB_REMEDIATION_DEADLINE = 18;
+const COL_BB_SOURCE = 19;
+
+function importBugBountyVulnerabilitiesToSheet(){
+ const ui = SpreadsheetApp.getUi();
+ let allJiraBounties = [];
+ try {
+ const {sheetID, JiraURL, JiraEmail, JiraAPIToken, JiraBugBountySheetName} = getJiraScriptProperties();
+
+
+ jiraBounties = getJiraVulnerabilityImportsforOrg(JiraURL, JiraEmail, JiraAPIToken);
+ //ui.alert(`Found ${jiraBounties.length} bounties in Jira.`)
+ for(i=0;i {
+ //jiraBounties.forEach(issue => {
+
+ const fields=issue.fields;
+ //ui.alert(issue.fields);
+ //ui.alert(fields.customfield_10265);
+ allJiraBounties.push([
+ (fields.customfield_10265==null) ? '':fields.customfield_10265.value,
+ (fields.customfield_10266==null) ? '':fields.customfield_10266.value,
+ (fields.customfield_10233==null) ? '':fields.customfield_10233.value,
+ fields.summary,
+ new Date(fields.customfield_10463),
+ new Date(fields.customfield_10465),
+ (fields.customfield_10232==null) ? '':fields.customfield_10232[0].value,
+ fields.customfield_10234,
+ new Date(fields.customfield_10464),
+ 'Placeholder Status',
+ (fields.customfield_10339==null)?'':fields.customfield_10339.value,
+ issue.key,
+ `${JiraURL}/browse/${issue.key}`,
+ (fields.customfield_10364==null)?'':fields.customfield_10364[0].value,
+ (fields.customfield_10365==null)?'':fields.customfield_10365[0].value,
+ fields.labels.toString(),
+ 'Placeholder - Days Opened',
+ 'Placeholder - Remediation Deadline',
+ 'Jira Bug Bounty'
+ ]);
+ });
+ }
+
+ const spreadsheet = SpreadsheetApp.openById(sheetID);
+ const sheet = spreadsheet.getSheetByName(JiraBugBountySheetName);
+ sheet.clear();
+ const headers = [ 'Team Name', 'Project Name', 'Severity', 'Summary', 'CreatedAt', 'TriagedAt', 'Vulnerability Confirmation', 'Payout Amount', 'FixedAt', 'Status', 'Status - Extended', 'Vulnerability ID', 'Vulnerability Link', 'PII Exposure', 'Financial Risk', 'Labels', 'Days Opened', 'Remediation Deadline', 'Source'];
+ sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
+ sheet.getRange(2,1, allJiraBounties.length, headers.length).setValues(allJiraBounties);
+
+
+ //Updating Status
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating status...').setTitle('Import Progress'));
+ //Set Status - Read value of status extended and set appropriate value.
+ sheet.getRange(2,COL_BB_STATUS,allJiraBounties.length,1).setFormulaR1C1("=If(or(R[0]C[1]=\"Risk Accepted\",R[0]C[1]=\"False Positive\"),\"Dismissed\",If(Or(R[0]C[1]=\"In Progress\",R[0]C[1]=\"Under Review\"),\"Open\",If(R[0]C[1]=\"Remediated\",\"Fixed\",\"\")))"); //Set reference formulas for calculated data - Team name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating days opened calculation...').setTitle('Import Progress'));
+ //Set Days Opened calculation - =If(Not(isBlank(FIXED_AT)),datedif(CREATED_AT,FIXED_AT,"D"),datedif(CREATED_AT,today(),"D"))
+ sheet.getRange(2,COL_BB_DAYS_OPENED,allJiraBounties.length,1).setFormulaR1C1('=If(Not(isBlank(R[0]C[-8])),datedif(R[0]C[-12],R[0]C[-8],"D"),datedif(R[0]C[-12],today(),"D"))');
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating remediation deadline calculation...').setTitle('Import Progress'));
+ //Set Remediation Deadline Calculation - =CREATED_AT+XLOOKUP(SEVERITY,RemediationPolicyTimelines!$A$1:$A$4,RemediationPolicyTimelines!$B$1:$B$4)
+ sheet.getRange(2,COL_BB_REMEDIATION_DEADLINE,allJiraBounties.length,1).setFormulaR1C1('=R[0]C[-13]+XLOOKUP(R[0]C[-15],RemediationPolicyTimelines!R1C1:R4C1,RemediationPolicyTimelines!R1C2:R4C2)');
+
+
+ //DATA CLEANUP and Normalization
+ startRow = 2;
+ lastRow = allJiraBounties.length
+ var valueToDelete = new Date(null);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Cleaning up Triaged At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Triaged At' Column
+ columnNumber = COL_BB_TRIAGED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up Fixed At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Fixed At' Column
+ var columnNumber = COL_BB_FIXED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ //Data Normalization
+ const BB_SEV_CRITICAL = 'Critical';
+ const BB_SEV_HIGH = 'High';
+ const BB_SEV_MODERATE = 'Medium';
+ const BB_SEV_LOW = 'Low';
+ const BB_SEV_INFORMATIONAL = 'Informational'
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Normalizing severity ratings...
').setTitle('Import Progress'));
+
+ columnNumber = COL_BB_SEVERITY;
+ var dataNormalizationRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var dataNormalizationValues = dataNormalizationRange.getValues();
+ //Iterate through the Values and update them to the global standard
+ for(var i = 0; iUpdating Team <-> Repo mapping...').setTitle('Import Progress'));
+ //Set Team Repo Mapping
+ sheet.getRange(2,COL_CS_TEAM_NAME,allCodeScanningVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1)),\"\",xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1))"); //Set reference formulas for calculated data - Team name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Project <-> Repo mapping...').setTitle('Import Progress'));
+ //Set Project Repo Mapping
+ sheet.getRange(2,COL_CS_PROJECT_NAME,allCodeScanningVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2)),\"\",xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2))"); //Set reference formulas for calculated data - Project name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating days opened calculation...').setTitle('Import Progress'));
+ //Set Days Opened calculation - =If(Not(isBlank(DISMISSED_AT)),datedif(CREATED_AT,DISMISSED_AT,"D"),If(Not(isBlank(FIXED_AT)),datedif(CREATED_AT,FIXED_AT,"D"), datedif(CREATED_AT,today(),"D")))
+ sheet.getRange(2,COL_CS_DAYS_OPENED,allCodeScanningVulnerabilities.length,1).setFormulaR1C1('=If(Not(isBlank(R[0]C[-8])),datedif(R[0]C[-9],R[0]C[-8],"D"),If(Not(isBlank(R[0]C[-4])),datedif(R[0]C[-9],R[0]C[-4],"D"),datedif(R[0]C[-9],today(),"D")))');
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Updating remediation deadline calculation...').setTitle('Import Progress'));
+ //Set Remediation Deadline Calculation - =CREATED_AT+XLOOKUP(SEVERITY,RemediationPolicyTimelines!$A$1:$A$4,RemediationPolicyTimelines!$B$1:$B$4)
+ sheet.getRange(2,COL_CS_REMEDIATION_DEADLINE,allCodeScanningVulnerabilities.length,1).setFormulaR1C1('=R[0]C[-10]+XLOOKUP(R[0]C[-12],RemediationPolicyTimelines!R1C1:R4C1,RemediationPolicyTimelines!R1C2:R4C2)');
+
+ //DATA CLEANUP and Normalization
+ startRow = 2;
+ lastRow = allCodeScanningVulnerabilities.length
+ var valueToDelete = new Date(null);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('
Cleaning up Fixed At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Fixed At' Column
+ columnNumber = COL_CS_FIXED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+*/
+
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Jira Bounties Import complete!
').setTitle('Import Progress'));
+ ui.alert('Success', 'Jira bounty data has been imported successfully.', ui.ButtonSet.OK);
+
+ } catch (e) {
+ ui.alert('Error', e.message, ui.ButtonSet.OK);
+ Logger.log(e.toString());
+ }
+
+}
+
+/**
+ * Helper function to safely extract data from the complex Jira issue object.
+ * You can customize this to find your specific custom fields.
+ *
+ * @param {Object} issue - The Jira issue object.
+ * @param {string} fieldName - The name of the field we want (from FIELDS_TO_IMPORT).
+ * @param {string} jiraDomain - The Jira domain to build links.
+ * @return {string|Date} The extracted value.
+
+function getJiraField(issue, fieldName, jiraDomain) {
+ const fields = issue.fields;
+ try {
+ switch (fieldName) {
+ case "Key":
+ return issue.key;
+ case "Link":
+ return `https://${jiraDomain}/browse/${issue.key}`;
+ case "Summary":
+ return fields.summary;
+ case "Status":
+ return fields.status ? fields.status.name : "N/A";
+ case "Assignee":
+ return fields.assignee ? fields.assignee.displayName : "Unassigned";
+ case "Reporter":
+ return fields.reporter ? fields.reporter.displayName : "N/A";
+ case "Created":
+ return fields.created ? new Date(fields.created) : null;
+ case "Updated":
+ return fields.updated ? new Date(fields.updated) : null;
+
+ // --- Example for a custom field ---
+ // Jira custom fields have names like "customfield_10010".
+ // Find the name by exporting an issue as JSON from Jira.
+ // Let's say "Severity" is "customfield_10025".
+ case "Severity":
+ // For custom fields with a simple value (e.g., dropdown)
+ return fields.customfield_10025 ? fields.customfield_10025.value : "N/A";
+
+ // Default case for any other fields
+ default:
+ // Try to find a field with the exact name (case-insensitive)
+ for (const key in fields) {
+ if (key.toLowerCase() === fieldName.toLowerCase()) {
+ return fields[key];
+ }
+ }
+ return "Not Found";
+ }
+ } catch (e) {
+ Logger.log(`Error parsing field "${fieldName}" for issue ${issue.key}: ${e}`);
+ return "Parse Error";
+ }
+} */
diff --git a/security_dashboard/README.MD b/security_dashboard/README.MD
new file mode 100644
index 0000000..62fcc56
--- /dev/null
+++ b/security_dashboard/README.MD
@@ -0,0 +1,6 @@
+The Security Dashboard is designed to pull data from disparate sources, normalize the data, and surface vulnerability information to the engineering leadership.
+
+Key Metrics (still WIP)
+- MTTR
+- Vulnerability Trending by Severity over time
+
diff --git a/security_dashboard/SecretScanningImport.gs b/security_dashboard/SecretScanningImport.gs
new file mode 100644
index 0000000..a4c9d62
--- /dev/null
+++ b/security_dashboard/SecretScanningImport.gs
@@ -0,0 +1,186 @@
+/**
+ * Fetches vulnerability alerts for a specific repository.
+ * @param {string} secretsAPIUrl - The GitHub GraphQL API endpoint.
+ * @param {string} orgName - The name of the organization.
+ * @param {string} repoName - The name of the repository.
+ * @param {string} token - The GitHub Personal Access Token.
+ * @returns {Array} A list of vulnerability alert objects.
+ */
+function getSecretsVulnerabilitiesForOrg(orgName, token) {
+ let secretsAPIUrl = "https://api.github.com/orgs/"+orgName+"/secret-scanning/alerts";
+ let vulnerabilities = [];
+ let hasNextPage = true;
+ let cursor = null;
+ const ui = SpreadsheetApp.getUi();
+
+ //while (hasNextPage) {
+ //const variables = { org: orgName, repo: repoName, cursor: cursor };
+ //const payload = JSON.stringify({ query, variables });
+
+ const options = {
+ 'method': 'get',
+ 'contentType': 'application/json',
+ 'headers': {
+ 'Authorization': `Bearer ${token}`,
+ 'Accept': 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+
+ 'muteHttpExceptions': true
+ };
+
+ const response = UrlFetchApp.fetch(secretsAPIUrl, options);
+ const responseCode = response.getResponseCode();
+ const responseBody = response.getContentText();
+ //Logger.log(responseBody);
+ //ui.alert(responseBody);
+
+
+ if (responseCode !== 200) {
+ // Log the error response from GitHub for debugging
+ //Logger.log(`Error from GitHub API: ${responseBody}`); //Uncomment for debugging
+ throw new Error(`Failed to fetch data. GitHub API responded with code: ${responseCode}. Check Logs for details.`);
+ }
+
+ const secretsAlerts = JSON.parse(responseBody);
+
+ if (secretsAlerts.length === 0) {
+ //ui.alert('No secret scanning alerts found for repo::'+repoName);
+ //Logger.log('No secret scanning alerts found for repo::'+repoName);
+ //return;
+ }
+ //}
+ return secretsAlerts;
+
+}
+
+const COL_SS_TEAM_NAME = 1;
+const COL_SS_PROJECT_NAME = 2;
+const COL_SS_REPO_Name = 3;
+const COL_SS_SEVERITY = 4;
+const COL_SS_SECRET = 5;
+const COL_SS_SECRET_TYPE = 6;
+const COL_SS_CREATED_AT = 7;
+const COL_SS_RESOLVED_AT = 8;
+const COL_SS_RESOLUTION_COMMENT = 9;
+const COL_SS_RESOLVED_BY = 10;
+const COL_SS_FIXED_AT = 11;
+const COL_SS_STATE = 12;
+const COL_SS_ALERT_NUM = 13;
+const COL_SS_LINK_TO_ALERT = 14;
+const COL_SS_DAYS_OPENED = 15;
+const COL_SS_REMEDIATION_DEADLINE = 16;
+const COL_SS_SOURCE = 17;
+
+function importSecretScanningVulnerabilitiesToSheet() {
+ const ui = SpreadsheetApp.getUi();
+
+ try{
+ const { githubEnterpriseUrl, githubOrg, githubToken, sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName } = getScriptProperties();
+ const repoAPIUrl = `https://api.github.com/graphql`;
+
+ let allSecretsVulnerabilities = [];
+
+ const progressHtml = `Processing secrets alerts!
`;
+ ui.showSidebar(HtmlService.createHtmlOutput(progressHtml).setTitle('Import Progress'));
+
+ const vulnerabilities = getSecretsVulnerabilitiesForOrg(githubOrg, githubToken);
+ vulnerabilities.forEach(vuln => { //Severity set statically to High since no severity is assigned from GH
+ //ui.alert(vuln.number);
+ allSecretsVulnerabilities.push([
+ 'Placeholder - TeamName',
+ 'Placeholder - ProjectName',
+ vuln.repository.name,
+ 'High',
+ vuln.secret,
+ vuln.secret_type_display_name,
+ new Date(vuln.created_at),
+ new Date(vuln.resolved_at),
+ vuln.resolution_comment,
+ (vuln.resolved_by==null)?"":vuln.resolved_by.login,
+ new Date(vuln.resolved_at),
+ vuln.state,
+ vuln.number,
+ vuln.html_url,
+ 'Placeholder - Days Opened',
+ 'Placeholder - Remediation Deadline',
+ 'Github Secrets Scanning'
+ ]);
+ });
+
+ const spreadsheet = SpreadsheetApp.openById(sheetId);
+ const sheet = spreadsheet.getSheetByName(secretsScanningSheetName);
+ sheet.clear();
+ const headers = [ 'Team Name', 'Project Name', 'Repo Name', 'Severity', 'Secret', 'Secret Type', 'Created At', 'Resolved At', 'Resolution Comment', 'Resolved By', 'Fixed At', 'State', 'Alert Number', 'Link to Alert', 'Days Opened', 'Remediation Deadline', 'Source' ];
+ sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
+ sheet.getRange(2,1, allSecretsVulnerabilities.length, headers.length).setValues(allSecretsVulnerabilities);
+
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Team <-> Repo mapping...
').setTitle('Import Progress'));
+ //Set Team Repo Mapping
+ sheet.getRange(2,COL_SS_TEAM_NAME,allSecretsVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1)),\"\",xlookup(R[0]C[2],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C1:R300C1))"); //Set reference formulas for calculated data - Team name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating Project <-> Repo mapping...
').setTitle('Import Progress'));
+ //Set Project Repo Mapping
+ sheet.getRange(2,COL_SS_PROJECT_NAME,allSecretsVulnerabilities.length,1).setFormulaR1C1("=If(isna(xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2)),\"\",xlookup(R[0]C[1],\'Project-Repo Mappings\'!R1C3:R300C3,\'Project-Repo Mappings\'!R1C2:R300C2))"); //Set reference formulas for calculated data - Project name lookup
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating days opened calculation...
').setTitle('Import Progress'));
+ //Set Days Opened calculation - Included FixedAt to make dashboard lookups easier. Since the data is cloned, there is no need to validate both different sets of data in the formula, so only on If-Not-IsBlank cycle is required.
+ sheet.getRange(2,COL_SS_DAYS_OPENED,allSecretsVulnerabilities.length,1).setFormulaR1C1('=If(Not(isBlank(R[0]C[-4])),datedif(R[0]C[-8],R[0]C[-4],"D"),datedif(R[0]C[-8],today(),"D"))');
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Updating remediation deadline calculation...
').setTitle('Import Progress'));
+ //Set Remediation Deadline Calculation - =F2+XLOOKUP(D2,RemediationPolicyTimelines!$A$1:$A$4,RemediationPolicyTimelines!$B$1:$B$4)
+ sheet.getRange(2,COL_SS_REMEDIATION_DEADLINE,allSecretsVulnerabilities.length,1).setFormulaR1C1('=R[0]C[-9]+XLOOKUP(R[0]C[-12],RemediationPolicyTimelines!R1C1:R4C1,RemediationPolicyTimelines!R1C2:R4C2)');
+
+
+ //DATA CLEANUP and Normalization
+ startRow = 2;
+ lastRow = allSecretsVulnerabilities.length
+ var valueToDelete = new Date(null);
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up Resolved At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Resolved At' Column
+ columnNumber = COL_SS_RESOLVED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Cleaning up Fixed At empty dates...
').setTitle('Import Progress'));
+ //Clear out any default blank dates in 'Fixed At' Column
+ columnNumber = COL_SS_FIXED_AT;
+ var dateCleanupRange = sheet.getRange(startRow, columnNumber, lastRow, 1);
+ var values = dateCleanupRange.getValues();
+
+ // Iterate through the values and clear cells matching the target value
+ for (var i = 0; i < values.length; i++) {
+ if (values[i][0].getTime() == valueToDelete.getTime()) {
+ sheet.getRange(i + startRow, columnNumber).clearContent();
+ }
+ }
+
+ //Data Normalization
+
+ //Easter Egg
+ if(sheet.getRange("'Unmatched Repos'!AA1").getValue()==42)
+ {
+ ui.showSidebar(HtmlService.createHtmlOutput('Jockeying the chickens

').setTitle('Import Progress'));
+ Utilities.sleep(5000);
+ }
+
+ ui.showSidebar(HtmlService.createHtmlOutput('Secrets Import complete!
').setTitle('Import Progress'));
+ ui.alert('Success', 'Secret Scanning data has been imported successfully.', ui.ButtonSet.OK);
+
+
+
+ } catch (e) {
+ ui.alert('Error', e.message, ui.ButtonSet.OK);
+ Logger.log(e.toString());
+ }
+}
\ No newline at end of file
diff --git a/security_dashboard/dataSynch.gs b/security_dashboard/dataSynch.gs
new file mode 100644
index 0000000..f6caa40
--- /dev/null
+++ b/security_dashboard/dataSynch.gs
@@ -0,0 +1,327 @@
+/**
+ * Fetches the script properties set by the user.
+ * @returns {object} The script properties.
+ */
+function getDataSynchScriptProperties() {
+ const properties = PropertiesService.getScriptProperties();
+ const sheetId = properties.getProperty('SHEET_ID');
+ const dependabotSheetName = properties.getProperty('GH_DEPENDABOT_SHEET_NAME');
+ const secretsScanningSheetName = properties.getProperty('GH_SECRETSSCANNING_SHEET_NAME');
+ const codeScanningSheetName = properties.getProperty('GH_CODESCANNING_SHEET_NAME');
+ const jiraBugBountySheetName = properties.getProperty('JIRA_BUGBOUNTY_SHEET_NAME');
+ const auditSheetName = properties.getProperty('AUDIT_SHEET_NAME');
+ const allVulnsSheetName = properties.getProperty('ALL_VULNS_SHEET_NAME')
+ if (!sheetId || !dependabotSheetName || !secretsScanningSheetName || !codeScanningSheetName || !jiraBugBountySheetName || !auditSheetName || !allVulnsSheetName) {
+ throw new Error("One or more script properties are not set. Please configure them in Project Settings.");
+ }
+
+ return { sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName, jiraBugBountySheetName, auditSheetName, allVulnsSheetName };
+}
+
+
+function dataSynch_AllVulnerabilities() {
+ const ui = SpreadsheetApp.getUi();
+
+ //General Approach - Import each data source sheet one at a time in order, inserting spaces in columns where there is no data
+ // - Dependabot
+ // - Secrets Scanning
+ // - Code scanning
+ // - Jira Bug Bounty
+ //Pull values first for all
+ //Backfill afterwards with R1C1 formulas for appropriate columns - Formulas for each column should be consistent across all data sources once merged
+
+ try {
+ const { sheetId, dependabotSheetName, secretsScanningSheetName, codeScanningSheetName, jiraBugBountySheetName, auditSheetName, allVulnsSheetName } = getDataSynchScriptProperties();
+
+ const spreadsheet = SpreadsheetApp.openById(sheetId);
+ const allVulnsSheet = spreadsheet.getSheetByName(allVulnsSheetName);
+ const dependabotSheet = spreadsheet.getSheetByName(dependabotSheetName);
+ const secretsScanningSheet = spreadsheet.getSheetByName(secretsScanningSheetName);
+ const codeScanningSheet = spreadsheet.getSheetByName(codeScanningSheetName);
+ const jiraBugBountySheet = spreadsheet.getSheetByName(jiraBugBountySheetName);
+ const auditImportsSheet = spreadsheet.getSheetByName(auditSheetName);
+
+ if (!allVulnsSheet) {
+ throw new Error(`Sheet with name "${allVulnsSheetName}" not found.`);
+ }
+
+ // Clear existing data and set headers
+ allVulnsSheet.clear();
+ const headers = ['Team Name', 'Project Name', 'Repo Name', 'Package', 'Severity', 'Summary', 'Secret Type', 'Created At', 'Triaged At', 'Vulnerability Confirmation', 'Payout Amount', 'Auto Dismissed At', 'Dismissed At', 'Dismiss Comment', 'Dismisser Name', 'Dismiss Reason', 'Fixed At', 'Status', 'Status - Extended', 'Vulnerability ID', 'Vulnerability Link', 'Vulnerable Filename', 'Vulnerable Filepath', 'Vulnerable Requirements', 'PII Exposure', 'Financial Risk', 'Labels', 'Days Opened', 'Remediation Deadline', 'Source' ];
+ allVulnsSheet.getRange(1, 1, 1, headers.length).setValues([headers]);
+
+ //Column identifier AV for All Vulnerabilities
+ const COL_AV_TEAM_NAME = 1;
+ const COL_AV_PROJECT_NAME = 2;
+ const COL_AV_REPO_NAME = 3;
+ const COL_AV_PACKAGE = 4;
+ const COL_AV_SEVERITY = 5;
+ const COL_AV_SUMMARY = 6;
+ const COL_AV_SECRET_TYPE = 7;
+ const COL_AV_CREATED_AT = 8;
+ const COL_AV_TRIAGED_AT = 9;
+ const COL_AV_VULNERABILITY_CONFIRMATION = 10;
+ const COL_AV_PAYOUT_AMOUNT = 11;
+ const COL_AV_AUTO_DISMISSED_AT = 12;
+ const COL_AV_DISMISSED_AT = 13;
+ const COL_AV_DISMISS_COMMENT = 14;
+ const COL_AV_DISMISSER_NAME = 15;
+ const COL_AV_DISMISS_REASON = 16;
+ const COL_AV_FIXED_AT = 17;
+ const COL_AV_STATUS = 18;
+ const COL_AV_STATUS_EXTENDED = 19;
+ const COL_AV_VULNERABILITY_ID = 20;
+ const COL_AV_VULNERABILITY_LINK = 21;
+ const COL_AV_VULNERABLE_FILENAME = 22;
+ const COL_AV_VULNERABLE_FILEPATH = 23;
+ const COL_AV_VULNERABLE_REQUIREMENTS = 24;
+ const COL_AV_PII_EXPOSURE = 25;
+ const COL_AV_FINANCIAL_RISK = 26;
+ const COL_AV_LABELS = 27;
+ const COL_AV_DAYS_OPENED = 28;
+ const COL_AV_REMEDIATION_DEADLINE = 29;
+ const COL_AV_SOURCE = 30;
+
+ let allVulns = [];
+ ui.showSidebar(HtmlService.createHtmlOutput('Merging dependabot results into universal tab...
').setTitle('Import Progress'));
+ //Load Dependabot Vulns
+ const dependabotRange = dependabotSheet.getDataRange();
+ var lastRow = dependabotRange.getLastRow();
+ var dependabotVulns = dependabotRange.getValues();
+ for(i=1;iMerging secret scanning results into universal tab...').setTitle('Import Progress'));
+ // - Secrets Scanning
+ const secretsScanningRange = secretsScanningSheet.getDataRange();
+ lastRow = secretsScanningRange.getLastRow();
+ secretsScanningVulns = secretsScanningRange.getValues();
+ for(i=1;iMerging code scanning results into universal tab...').setTitle('Import Progress'));
+ // - Code Scanning
+ const codeScanningRange = codeScanningSheet.getDataRange();
+ lastRow = codeScanningRange.getLastRow();
+ codeScanningVulns = codeScanningRange.getValues();
+ for(i=1;iMerging bug bounty results into universal tab...').setTitle('Import Progress'));
+ // - Jira Bug Bounty
+ const jiraBugbountyRange = jiraBugBountySheet.getDataRange();
+ var lastRow = jiraBugbountyRange.getLastRow();
+ var jiraBugBountyVulns = jiraBugbountyRange.getValues();
+ for(var i=1;iMerging audit results into universal tab...').setTitle('Import Progress'));
+ // - Audit Imports
+ const auditImportsRange = auditImportsSheet.getDataRange();
+ lastRow = auditImportsRange.getLastRow();
+ var auditImportsVulns = auditImportsRange.getValues();
+ for(var i=1;i 0) {
+ allVulnsSheet.getRange(2, 1, allVulns.length, headers.length).setValues(allVulns);
+ }
+
+ const startRow = 2;
+ lastRow = allVulns.length;
+ columnNumber = COL_AV_SEVERITY;
+ var dataNormalizationRange = allVulnsSheet.getRange(startRow, columnNumber, lastRow, 1);
+ var dataNormalizationValues = dataNormalizationRange.getValues();
+ //Iterate through the Values and update them to the global standard
+ for(var i = 0; iData Synch complete!').setTitle('Import Progress'));
+ ui.alert('Success', 'Vulnerabilities have been synched to the universal tab successfully.', ui.ButtonSet.OK);
+
+ } catch (e) {
+ ui.alert('Error', e.message, ui.ButtonSet.OK);
+ Logger.log(e.toString());
+ }
+}