diff --git a/vss-extension-dev.json b/vss-extension-dev.json index 18f955c..e2f53cf 100644 --- a/vss-extension-dev.json +++ b/vss-extension-dev.json @@ -1,7 +1,7 @@ { "manifestVersion": 1, "id": "GHAzDoWidget-DEV", - "version": "0.2.404", + "version": "0.2.415", "public": false, "name": "Advanced Security dashboard Widgets [DEV]", "description": "[DEV] GitHub Advanced Security for Azure DevOps dashboard widgets", diff --git a/vss-extension.json b/vss-extension.json index 8e51870..37224bd 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -1,7 +1,7 @@ { "manifestVersion": 1, "id": "GHAzDoWidget", - "version": "0.0.1.17", + "version": "0.0.1.19", "public": true, "name": "Advanced Security dashboard Widgets", "description": "GitHub Advanced Security for Azure DevOps dashboard widgets", diff --git a/widgets/library.js b/widgets/library.js index f950083..6af3bfe 100644 --- a/widgets/library.js +++ b/widgets/library.js @@ -187,7 +187,7 @@ async function storeAlerts(repoId, alertResult) { } } -async function getAlertsTrendLines(organization, projectName, repoId, daysToGoBack, summaryBucket) { +async function getAlertsTrendLines(organization, projectName, repoId, daysToGoBack, summaryBucket, alertType, overviewType = false, showClosed = false) { consoleLog(`getAlertsTrend for organization [${organization}], project [${projectName}], repo [${repoId}]`) try { alertResult = null @@ -214,23 +214,40 @@ async function getAlertsTrendLines(organization, projectName, repoId, daysToGoBa codeAlertsTrend: [] } } + if (overviewType === false) { + consoleLog('Loading all alerts trend lines') + // load the Secret alerts and create a trend line over the last 3 weeks + const secretAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.SECRET.name) + const secretAlertsTrend = getAlertsTrendLine(secretAlerts, AlertType.SECRET.name, daysToGoBack, summaryBucket) + consoleLog('') + // load the Dependency alerts and create a trend line over the last 3 weeks + const dependencyAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.DEPENDENCY.name) + const dependencyAlertsTrend = getAlertsTrendLine(dependencyAlerts, AlertType.DEPENDENCY.name, daysToGoBack, summaryBucket) + consoleLog('') + // load the Code alerts and create a trend line over the last 3 weeks + const codeAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.CODE.name) + const codeAlertsTrend = getAlertsTrendLine(codeAlerts, AlertType.CODE.name, daysToGoBack, summaryBucket) - // load the Secret alerts and create a trend line over the last 3 weeks - const secretAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.SECRET.name) - const secretAlertsTrend = getAlertsTrendLine(secretAlerts, AlertType.SECRET.name, daysToGoBack, summaryBucket) - consoleLog('') - // load the Dependency alerts and create a trend line over the last 3 weeks - const dependencyAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.DEPENDENCY.name) - const dependencyAlertsTrend = getAlertsTrendLine(dependencyAlerts, AlertType.DEPENDENCY.name, daysToGoBack, summaryBucket) - consoleLog('') - // load the Code alerts and create a trend line over the last 3 weeks - const codeAlerts = alertResult.value.filter(alert => alert.alertType === AlertType.CODE.name) - const codeAlertsTrend = getAlertsTrendLine(codeAlerts, AlertType.CODE.name, daysToGoBack, summaryBucket) + return { + secretAlertsTrend: secretAlertsTrend, + dependencyAlertsTrend: dependencyAlertsTrend, + codeAlertsTrend: codeAlertsTrend + } + } + else { + consoleLog(`Loading alerts trend lines for alert type: [$alertType.name}]`) + // filter the alerts based on the alertType + const alerts = alertResult.value.filter(alert => alert.alertType === alertType.name) + // create a trend line over the last 3 weeks for number of open alerts + const alertsOpenTrend = getAlertsTrendLine(alerts, alertType.name, daysToGoBack, summaryBucket, 'open') + const alertsDismissedTrend = getAlertsTrendLine(alerts, alertType.name, daysToGoBack, summaryBucket, 'dismissed') + const alertFixedTrend = getAlertsTrendLine(alerts, alertType.name, daysToGoBack, summaryBucket, 'fixed') - return { - secretAlertsTrend: secretAlertsTrend, - dependencyAlertsTrend: dependencyAlertsTrend, - codeAlertsTrend: codeAlertsTrend + return { + alertsOpenTrend: alertsOpenTrend, + alertsDismissedTrend: alertsDismissedTrend, + alertFixedTrend: alertFixedTrend + } } } catch (err) { @@ -258,7 +275,29 @@ function checkAlertActiveOnDate(alert, dateStr) { return seenClosed; } -function getAlertsTrendLine(alerts, type, daysToGoBack = 21, summaryBucket = 1) { +function checkAlertDismissedOnDate(alert, dateStr) { + // check if the alert.firstSeenDate is within the date range + // and if fixedDate is not set or is after the date range + const seenClosed = (alert.firstSeenDate.split('T')[0] <= dateStr && (!alert.fixedDate || alert.fixedDate.split('T')[0] > dateStr)); + if (seenClosed) { + // check the dismissal.requestedOn date as well + if (alert.dismissal && alert.dismissal.requestedOn) { + const dismissed = (alert.dismissal.requestedOn.split('T')[0] <= dateStr); + return dismissed; + } + } + + return false; +} + +function checkAlertFixedOnDate(alert, dateStr) { + // check if the alert.firstSeenDate is within the date range + // and if fixedDate is not set or is after the date range + const seenClosed = (alert.firstSeenDate.split('T')[0] <= dateStr && (alert.fixedDate && alert.fixedDate.split('T')[0] < dateStr)); + return seenClosed; +} + +function getAlertsTrendLine(alerts, type, daysToGoBack = 21, summaryBucket = 1, filter = 'open') { consoleLog(`getAlertsTrendLine for type ${type}`); const trendLine = []; @@ -270,9 +309,23 @@ function getAlertsTrendLine(alerts, type, daysToGoBack = 21, summaryBucket = 1) for (let d = startDate; d <= today; d.setDate(d.getDate() + summaryBucket)) { const date = new Date(d); const dateStr = date.toISOString().split('T')[0]; - - const alertsOnDate = alerts.filter(alert => checkAlertActiveOnDate(alert, dateStr)); - console.log(`On [${dateStr}] there were [${alertsOnDate.length}] active ${type} alerts`); + let alertsOnDate = [] + switch (filter) { + case 'open': + alertsOnDate = alerts.filter(alert => checkAlertActiveOnDate(alert, dateStr)); + break; + case 'dismissed': + alertsOnDate = alerts.filter(alert => checkAlertDismissedOnDate(alert, dateStr)); + break; + case 'fixed': + alertsOnDate = alerts.filter(alert => checkAlertFixedOnDate(alert, dateStr)); + break; + default: + // get all active alerts on this date + alertsOnDate = alerts.filter(alert => checkAlertActiveOnDate(alert, dateStr)); + break; + } + console.log(`On [${dateStr}] there were [${alertsOnDate.length}] [${filter}] [${type}] alerts`); trendLine.push({ date: dateStr, count: alertsOnDate.length diff --git a/widgets/widgets/chart/chart.html b/widgets/widgets/chart/chart.html index b949ab8..b26badc 100644 --- a/widgets/widgets/chart/chart.html +++ b/widgets/widgets/chart/chart.html @@ -11,7 +11,7 @@ VSS.init({ explicitNotifyLoaded: true, usePlatformStyles: true - }); + }) VSS.require([ "TFS/Dashboards/WidgetHelpers", @@ -26,52 +26,62 @@ return { load: async function(widgetSettings) { return Services.ChartsService.getService().then(async function(chartService){ - consoleLog("Starting to create chart"); - var $container = $('#Chart-Container'); - var $title = $('h2.ghazdo-title'); + consoleLog("Starting to create chart") + var $container = $('#Chart-Container') + var $title = $('h2.ghazdo-title') - const webContext = VSS.getWebContext(); - const project = webContext.project; - const organization = webContext.account.name; - const projectId = project.id; + const webContext = VSS.getWebContext() + const project = webContext.project + const organization = webContext.account.name + const projectId = project.id // convert project.name to url encoding - const projectName = project.name.replace(/ /g, "%20").replace(/&/g, "%26"); + const projectName = project.name.replace(/ /g, "%20").replace(/&/g, "%26") - consoleLog('project id: ' + projectId); - consoleLog('project name: ' + projectName); - consoleLog('organization name: ' + organization); + consoleLog('project id: ' + projectId) + consoleLog('project name: ' + projectName) + consoleLog('organization name: ' + organization) - consoleLog(`WidgetSettings inside loadChartWidget_2x2: ${JSON.stringify(widgetSettings)}`); + consoleLog(`WidgetSettings inside loadChartWidget_2x2: ${JSON.stringify(widgetSettings)}`) // data contains a stringified json object, so we need to make a json object from it - const data = JSON.parse(widgetSettings.customSettings.data); + const data = JSON.parse(widgetSettings.customSettings.data) let repoName let repoId // init empty object first - let alertTrendLines = {secretAlertTrend: [], dependencyAlertTrend: [], codeAlertsTrend: []}; - let chartType = 1; - let alertTypeConfig = 1; + let alertTrendLines = {secretAlertTrend: [], dependencyAlertTrend: [], codeAlertsTrend: []} + let chartType = 1 + let alertTypeConfig = 1 if (data && data.chartType && data.chartType !== "") { - chartType = data.chartType; - consoleLog('loaded chartType from widgetSettings: ' + chartType); + chartType = data.chartType + consoleLog('loaded chartType from widgetSettings: ' + chartType) } else { - consoleLog('chartType is not set, using default value: ' + chartType); + consoleLog('chartType is not set, using default value: ' + chartType) } if (data && data.alertType) { - alertTypeConfig = data.alertType; - consoleLog('loaded alertType from widgetSettings: ' + alertTypeConfig); + alertTypeConfig = data.alertType + consoleLog('loaded alertType from widgetSettings: ' + alertTypeConfig) } if (data && data.repo && data.repo !== "") { - repoName = data.repo; - repoId = data.repoId; + repoName = data.repo + repoId = data.repoId $container.text(`${data.repo}`) switch (chartType) { + case "3": + try { + const alertType = GetAlertTypeFromValue(alertTypeConfig) + $title.text(`${alertType.display} Alerts status trend`) + renderDurationChart({organization, projectName, repoId, $container, chartService, alertType, widgetSize: widgetSettings.size}) + } + catch (err) { + consoleLog(`Error loading the alerts pie: ${err}`) + } + break case "2": try { const alertType = GetAlertTypeFromValue(alertTypeConfig); @@ -79,32 +89,32 @@ renderPieChart(organization, projectName, repoId, $container, chartService, alertType, widgetSettings.size); } catch (err) { - consoleLog(`Error loading the alerts pie: ${err}`); + consoleLog(`Error loading the alerts pie: ${err}`) } - break; + break default: try { - $title.text(`Advanced Security Alerts Trend`) - renderTrendLine(organization, projectName, repoId, $container, chartService, widgetSettings.size); + $title.text(`Advanced Security alerts trend`) + renderTrendLine(organization, projectName, repoId, $container, chartService, widgetSettings.size) } catch (err) { - consoleLog(`Error loading the alerts trend: ${err}`); + consoleLog(`Error loading the alerts trend: ${err}`) } - break; + break } } else { - consoleLog('configuration is needed first, opening with empty values'); + consoleLog('configuration is needed first, opening with empty values') // set the tile to indicate config is needed - $title.text(`Configure the widget to get Advanced Security alerts trend information`); + $title.text(`Configure the widget to get Advanced Security alerts trend information`) } - return WidgetHelpers.WidgetStatusHelper.Success(); - }); + return WidgetHelpers.WidgetStatusHelper.Success() + }) } } - }); - VSS.notifyLoadSucceeded(); + }) + VSS.notifyLoadSucceeded() }); diff --git a/widgets/widgets/chart/chart.js b/widgets/widgets/chart/chart.js index 6044a7e..f630e12 100644 --- a/widgets/widgets/chart/chart.js +++ b/widgets/widgets/chart/chart.js @@ -32,27 +32,27 @@ async function createChart($container, chartService, alertTrendLines, widgetSize }; try { - chartService.createChart($container, chartOptions); + chartService.createChart($container, chartOptions) } catch (err) { - console.log(`Error creating line chart: ${err}`); + console.log(`Error creating line chart: ${err}`) } } function getChartWidthFromWidgetSize(widgetSize) { // a column is 160px wide, and gutters are 10px wide, and there is 1 14px margins on the right side to handle - return 160 * widgetSize.columnSpan + (10 * (widgetSize.columnSpan -1)) - (1 * 14); + return 160 * widgetSize.columnSpan + (10 * (widgetSize.columnSpan -1)) - (1 * 14) } async function createPieChart($container, chartService, alertSeverityCount, widgetSize) { // convert alertSeverityCount to two arrays, one for the labels and one for the data - consoleLog(`createPieChart for alertSeverityCount: ${JSON.stringify(alertSeverityCount)}`); - const labels = []; - const data = []; + consoleLog(`createPieChart for alertSeverityCount: ${JSON.stringify(alertSeverityCount)}`) + const data = [] + const labels = [] for (const index in alertSeverityCount) { - const item = alertSeverityCount[index]; - labels.push(item.severity); - data.push(item.count); + const item = alertSeverityCount[index] + labels.push(item.severity) + data.push(item.count) } var chartOptions = { @@ -71,26 +71,88 @@ async function createPieChart($container, chartService, alertSeverityCount, widg "showLabels": "true", "size": 200 } - }; + } try { - chartService.createChart($container, chartOptions); + chartService.createChart($container, chartOptions) + } + catch (err) { + console.log(`Error creating pie chart: ${err}`) + } +} + +async function createDurationChart($container, chartService, alertSeverityCount, widgetSize) { + consoleLog(`createDurationChart for alertSeverityCount: ${JSON.stringify(alertSeverityCount)}`) + const datePoints = getDatePoints(); + const alertsOpenTrend = alertSeverityCount.alertsOpenTrend + const alertsDismissedTrend = alertSeverityCount.alertsDismissedTrend + const alertFixedTrend = alertSeverityCount.alertFixedTrend + + var chartOptions = { + "hostOptions": { + "height": "290", + "width": getChartWidthFromWidgetSize(widgetSize) + }, + "chartType": "stackedArea", + "series": [ + { + "name": "Open", + "data": alertsOpenTrend + }, + { + "name": "Dismissed", + "data": alertsDismissedTrend + }, + { + "name": "Fixed", + "data": alertFixedTrend + } + ], + "xAxis": { + "labelValues": datePoints, + "labelFormatMode": "dateTime", // format is 2023-09-17 + }, + "specializedOptions": { + "showLabels": "true", + "size": 200 + } + } + + try { + chartService.createChart($container, chartOptions) + } + catch (err) { + console.log(`Error creating pie chart: ${err}`) + } +} + +async function renderDurationChart({organization, projectName, repoId, $container, chartService, alertType, widgetSize}) { + consoleLog(`renderDurationChart for alertType: [${alertType.name}]`) + try { + // get the trend data for alerts first + const showClosed = true + const overviewType = true + const daysToGoBack = 21 + const summaryBucket = 1 + const alertTrendLines = await getAlertsTrendLines(organization, projectName, repoId, daysToGoBack, summaryBucket, alertType, overviewType, showClosed) + + createDurationChart($container, chartService, alertTrendLines, widgetSize) } catch (err) { - console.log(`Error creating pie chart: ${err}`); + consoleLog(`Error loading the alerts trend: ${err}`) } } async function renderPieChart(organization, projectName, repoId, $container, chartService, alertType, widgetSize) { - consoleLog('renderPieChart'); + consoleLog('renderPieChart') try { // get the trend data for alerts first - const alertSeverityCount = await getAlertSeverityCounts(organization, projectName, repoId, alertType); + const alertSeverityCount = await getAlertSeverityCounts(organization, projectName, repoId, alertType) - createPieChart($container, chartService, alertSeverityCount, widgetSize); + createPieChart($container, chartService, alertSeverityCount, widgetSize) } catch (err) { - consoleLog(`Error loading the alerts pie: ${err}`); + consoleLog(`Error loading the alerts pie: ${err}`) } } @@ -106,6 +168,6 @@ async function renderTrendLine(organization, projectName, repoId, $container, ch createChart($container, chartService, alertTrendLines, widgetSize, daysToGoBack, summaryBucket); } catch (err) { - consoleLog(`Error loading the alerts trend: ${err}`); + consoleLog(`Error loading the alerts trend: ${err}`) } } \ No newline at end of file diff --git a/widgets/widgets/chart/configuration_2x2.html b/widgets/widgets/chart/configuration_2x2.html index 77f1d7b..d2abcf9 100644 --- a/widgets/widgets/chart/configuration_2x2.html +++ b/widgets/widgets/chart/configuration_2x2.html @@ -136,6 +136,7 @@