Skip to content

Commit

Permalink
Merge pull request #10 from rajbos/chart
Browse files Browse the repository at this point in the history
Adding trend line chart for the AdvSec alerts
  • Loading branch information
rajbos authored Sep 17, 2023
2 parents a4c4041 + eeb56a8 commit fb6047a
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 4 deletions.
140 changes: 140 additions & 0 deletions chart/chart.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<script src="../lib/VSS.SDK.min.js"></script>
<script src="../library.js"></script>
<link rel="stylesheet" href="../styles.css" />
</head>
<body>
<script lang="">
VSS.init({
explicitNotifyLoaded: true,
usePlatformStyles: true
});

VSS.require([
"TFS/Dashboards/WidgetHelpers",
"Charts/Services",
"VSS/Context",
"VSS/Authentication/Services"
],
function (WidgetHelpers, Services, context) {
WidgetHelpers.IncludeWidgetStyles();
VSS.register("GHAzDoWidget.Chart", function () {
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.title');

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");

consoleLog('project id: ' + projectId);
consoleLog('project name: ' + projectName);
consoleLog('organization name: ' + organization);

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);

let repoName
let repoId
if (data && data.repo) {
repoName = data.repo;
repoId = data.repoId;
consoleLog('loaded repoName from widgetSettings: ' + repoName);

$title.text(`Advanced Security Alerts Trend`)
$container.text(`${data.repo}`)

// load the alerts for the selected repo
try {
alerts = await getAlerts(organization, projectName, repoId);
consoleLog('alerts: ' + JSON.stringify(alerts));
}
catch (err) {
consoleLog(`Error loading the grouped alerts: ${err}`);
}
}
else {
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`);
}

// init empty object first
let alertTrendLines = {secretAlertTrend: [], dependencyAlertTrend: [], codeAlertsTrend: []};
try {
// get the trend data for alerts first
alertTrendLines = await getAlertsTrendLines(organization, projectName, repoId)
consoleLog('Dependencies AlertTrend: ' + JSON.stringify(alertTrendLines.dependencyAlertsTrend));
consoleLog('Code scanning AlertTrend: ' + JSON.stringify(alertTrendLines.codeAlertsTrend));
consoleLog('Secrets AlertTrend: ' + JSON.stringify(alertTrendLines.secretAlertsTrend));
}
catch (err) {
consoleLog(`Error loading the alerts trend: ${err}`);
}

const datePoints = getDatePoints();
var chartOptions = {
"hostOptions": {
"height": "290",
"width": "300"
},
"chartType": "line",
"series":
[
{
"name": "Dependencies",
"data": alertTrendLines.dependencyAlertsTrend
},
{
"name": "Code scanning",
"data": alertTrendLines.codeAlertsTrend
},
{
"name": "Secrets",
"data": alertTrendLines.secretAlertsTrend
}
],
"xAxis": {
"labelValues": datePoints,
"labelFormatMode": "dateTime", // format is 2023-09-17
},
"specializedOptions": {
"includeMarkers": "true"
}
};

try {
chartService.createChart($container, chartOptions);
}
catch (err) {
console.log(`Error creating chart: ${err}`);
}
return WidgetHelpers.WidgetStatusHelper.Success();
});
}
}
});
VSS.notifyLoadSucceeded();
});

function consoleLog(message) {
console.log(message);
}
</script>

<div class="widget">
<h2 class="title">Chart Widget</h2>
<div id="Chart-Container"></div>
</div>
</body>
</html>
107 changes: 107 additions & 0 deletions chart/configuration_2x2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="../lib/VSS.SDK.min.js"></script>
<link rel="stylesheet" href="../styles.css" />

<script type="text/javascript">
VSS.init({
explicitNotifyLoaded: true,
usePlatformStyles: true
});

VSS.require(["VSS/Service", "TFS/Dashboards/WidgetHelpers", "VSS/Context", "TFS/VersionControl/GitRestClient"],
function (Service, WidgetHelpers, context, GitWebApi) {
VSS.register("GHAzDoWidget.Chart.Configuration", function () {
var $repoDropdown = $("#repo-dropdown");

async function getRepos() {
try {
const webContext = VSS.getWebContext();
const project = webContext.project;

// todo: load the available repos in this project
const gitClient = Service.getClient(GitWebApi.GitHttpClient);
repos = await gitClient.getRepositories(project.name);
console.log(`Found these repos: ${JSON.stringify(repos)}`);
return repos;
}
catch (err) {
console.log(`Error loading the available repos: ${err}`);
return [];
}
}

return {
load: async function (widgetSettings, widgetConfigurationContext) {
var settings = JSON.parse(widgetSettings.customSettings.data);
console.log(`Loading the Chart.2x2 settings with ${JSON.stringify(settings)}`)

const repos = await getRepos();
// add all repos as selection options to the dropdown
if (repos) {
// sort the repo alphabetically
repos.sort((a, b) => a.name.localeCompare(b.name));
repos.forEach(r => {
$repoDropdown.append(`<option value=${r.name}>${r.name}</option>`);
});
}

if (settings && settings.repo) {
// select the repo that was saved in the settings
$repoDropdown.val(settings.repo);
}

$repoDropdown.on("change", function () {
let repo;
if (repos) {
// find the repo with this name
repo = repos.find(r => r.name === $repoDropdown.val());
}

var customSettings = {
data: JSON.stringify({
repo: $repoDropdown.val(),
repoId: repo.id
})
};
var eventName = WidgetHelpers.WidgetEvent.ConfigurationChange;
var eventArgs = WidgetHelpers.WidgetEvent.Args(customSettings);
widgetConfigurationContext.notify(eventName, eventArgs);
});

return WidgetHelpers.WidgetStatusHelper.Success();
},
onSave: async function() {
const repos = await getRepos();
let repo;
if (repos) {
// find the repo with this name
repo = repos.find(r => r.name === $repoDropdown.val());
}
var customSettings = {
data: JSON.stringify({
repo: $repoDropdown.val(),
repoId: repo.id
})
};
console.log(`Saving the Chart.2x2 settings with ${JSON.stringify(customSettings)}`)
return WidgetHelpers.WidgetConfigurationSave.Valid(customSettings);
}
}
});
VSS.notifyLoadSucceeded();
});
</script>
</head>
<body>
<div class="container">
<fieldset>
<label class="label">Repository: </label>
<select id="repo-dropdown" style="margin-top:10px">
<!-- todo: dynamically load the available repos in this project-->
</select>
</fieldset>
</div>
</body>
</html>
Binary file added img/example_chart_2x2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 92 additions & 2 deletions library.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ function authenticatedGet(url) {
.then(x => x.json());
}

async function getAlerts (organization, projectName, repoId) {
async function getAlerts(organization, projectName, repoId) {
consoleLog('getAlerts');

try{
// todo: add pagination
// no pagination option, so just get the first 5000 alerts
url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/AdvancedSecurity/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=truen&criteria.states=1&api-version=7.2-preview.1`;
consoleLog(`Calling url: [${url}]`);
const alertResult = await authenticatedGet(url);
Expand All @@ -51,4 +51,94 @@ async function getAlerts (organization, projectName, repoId) {
catch (err) {
consoleLog('error in calling the advec api: ' + err);
}
}

async function getAlertsTrendLines(organization, projectName, repoId) {
consoleLog(`getAlertsTrend for organization [${organization}], project [${projectName}], repo [${repoId}]`);

try {
url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/AdvancedSecurity/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=truen&api-version=7.2-preview.1`;
consoleLog(`Calling url: [${url}]`);
const alertResult = await authenticatedGet(url);
//consoleLog('alertResult: ' + JSON.stringify(alertResult));
consoleLog('alertResult count: ' + alertResult.count);

// load the Secret alerts and create a trend line over the last 3 weeks
const secretAlerts = alertResult.value.filter(alert => alert.alertType === "secret");
const secretAlertsTrend = getAlertsTrendLine(secretAlerts, 'secret');
console.log('');
// load the Dependency alerts and create a trend line over the last 3 weeks
const dependencyAlerts = alertResult.value.filter(alert => alert.alertType === "dependency");
const dependencyAlertsTrend = getAlertsTrendLine(dependencyAlerts, 'dependency');console.log('');
console.log('');
// load the Code alerts and create a trend line over the last 3 weeks
const codeAlerts = alertResult.value.filter(alert => alert.alertType === "code");
const codeAlertsTrend = getAlertsTrendLine(codeAlerts, 'code');

return {
secretAlertsTrend: secretAlertsTrend,
dependencyAlertsTrend: dependencyAlertsTrend,
codeAlertsTrend: codeAlertsTrend
};
}
catch (err) {
consoleLog('error in calling the advec api: ' + err);
}
}

function checkAlertActiveOnDate(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 seenClosed;
}

function getAlertsTrendLine(alerts, type) {
consoleLog(`getAlertsTrendLine for type ${type}`);

const trendLine = [];
const trendLineSimple = [];
const today = new Date();
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(today.getDate() - 21);

for (let d = threeWeeksAgo; d <= today; d.setDate(d.getDate() + 1)) {
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`);
trendLine.push({
date: dateStr,
count: alertsOnDate.length
});

trendLineSimple.push(alertsOnDate.length);
}

consoleLog('trendLine: ' + JSON.stringify(trendLineSimple));
return trendLineSimple;
}

function getDatePoints() {
const trendDates = [];
const today = new Date();
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(today.getDate() - 21);

for (let d = threeWeeksAgo; d <= today; d.setDate(d.getDate() + 1)) {
const date = new Date(d);
const dateStr = date.toISOString().split('T')[0];
trendDates.push(dateStr);
}

return trendDates;
}
3 changes: 3 additions & 0 deletions overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Install from the marketplace: https://marketplace.visualstudio.com/items?itemNam
### Split it into three separate widgets (1 by 1) with just the single value you scan for:
![Screenshot of the widget in 1 by 1 showing the repository name and the alert count for dependencies, secrets, and code scanning](/img/example_1x1.png)

### Show a trend line (2 by 2) of all alerts in the last 3 weeks:
![Screenshot of the chart widget in 2 by 2 showing the repository name and the alert count for dependencies, secrets, and code scanning](/img/example_chart_2x2.png)

## GitHub repo
Please report issues, feature request, and feedback here: https://github.com/rajbos/GHAzDo-widget.

Expand Down
Loading

0 comments on commit fb6047a

Please sign in to comment.