Skip to content

Commit fb6047a

Browse files
authored
Merge pull request #10 from rajbos/chart
Adding trend line chart for the AdvSec alerts
2 parents a4c4041 + eeb56a8 commit fb6047a

File tree

7 files changed

+416
-4
lines changed

7 files changed

+416
-4
lines changed

chart/chart.html

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="../lib/VSS.SDK.min.js"></script>
5+
<script src="../library.js"></script>
6+
<link rel="stylesheet" href="../styles.css" />
7+
</head>
8+
<body>
9+
<script lang="">
10+
VSS.init({
11+
explicitNotifyLoaded: true,
12+
usePlatformStyles: true
13+
});
14+
15+
VSS.require([
16+
"TFS/Dashboards/WidgetHelpers",
17+
"Charts/Services",
18+
"VSS/Context",
19+
"VSS/Authentication/Services"
20+
],
21+
function (WidgetHelpers, Services, context) {
22+
WidgetHelpers.IncludeWidgetStyles();
23+
VSS.register("GHAzDoWidget.Chart", function () {
24+
return {
25+
load: async function(widgetSettings) {
26+
return Services.ChartsService.getService().then(async function(chartService){
27+
consoleLog("Starting to create chart");
28+
var $container = $('#Chart-Container');
29+
var $title = $('h2.title');
30+
31+
const webContext = VSS.getWebContext();
32+
const project = webContext.project;
33+
const organization = webContext.account.name;
34+
const projectId = project.id;
35+
// convert project.name to url encoding
36+
const projectName = project.name.replace(/ /g, "%20").replace(/&/g, "%26");
37+
38+
consoleLog('project id: ' + projectId);
39+
consoleLog('project name: ' + projectName);
40+
consoleLog('organization name: ' + organization);
41+
42+
consoleLog(`WidgetSettings inside loadChartWidget_2x2: ${JSON.stringify(widgetSettings)}`);
43+
44+
// data contains a stringified json object, so we need to make a json object from it
45+
const data = JSON.parse(widgetSettings.customSettings.data);
46+
47+
let repoName
48+
let repoId
49+
if (data && data.repo) {
50+
repoName = data.repo;
51+
repoId = data.repoId;
52+
consoleLog('loaded repoName from widgetSettings: ' + repoName);
53+
54+
$title.text(`Advanced Security Alerts Trend`)
55+
$container.text(`${data.repo}`)
56+
57+
// load the alerts for the selected repo
58+
try {
59+
alerts = await getAlerts(organization, projectName, repoId);
60+
consoleLog('alerts: ' + JSON.stringify(alerts));
61+
}
62+
catch (err) {
63+
consoleLog(`Error loading the grouped alerts: ${err}`);
64+
}
65+
}
66+
else {
67+
consoleLog('configuration is needed first, opening with empty values');
68+
// set the tile to indicate config is needed
69+
$title.text(`Configure the widget to get Advanced Security alerts trend information`);
70+
}
71+
72+
// init empty object first
73+
let alertTrendLines = {secretAlertTrend: [], dependencyAlertTrend: [], codeAlertsTrend: []};
74+
try {
75+
// get the trend data for alerts first
76+
alertTrendLines = await getAlertsTrendLines(organization, projectName, repoId)
77+
consoleLog('Dependencies AlertTrend: ' + JSON.stringify(alertTrendLines.dependencyAlertsTrend));
78+
consoleLog('Code scanning AlertTrend: ' + JSON.stringify(alertTrendLines.codeAlertsTrend));
79+
consoleLog('Secrets AlertTrend: ' + JSON.stringify(alertTrendLines.secretAlertsTrend));
80+
}
81+
catch (err) {
82+
consoleLog(`Error loading the alerts trend: ${err}`);
83+
}
84+
85+
const datePoints = getDatePoints();
86+
var chartOptions = {
87+
"hostOptions": {
88+
"height": "290",
89+
"width": "300"
90+
},
91+
"chartType": "line",
92+
"series":
93+
[
94+
{
95+
"name": "Dependencies",
96+
"data": alertTrendLines.dependencyAlertsTrend
97+
},
98+
{
99+
"name": "Code scanning",
100+
"data": alertTrendLines.codeAlertsTrend
101+
},
102+
{
103+
"name": "Secrets",
104+
"data": alertTrendLines.secretAlertsTrend
105+
}
106+
],
107+
"xAxis": {
108+
"labelValues": datePoints,
109+
"labelFormatMode": "dateTime", // format is 2023-09-17
110+
},
111+
"specializedOptions": {
112+
"includeMarkers": "true"
113+
}
114+
};
115+
116+
try {
117+
chartService.createChart($container, chartOptions);
118+
}
119+
catch (err) {
120+
console.log(`Error creating chart: ${err}`);
121+
}
122+
return WidgetHelpers.WidgetStatusHelper.Success();
123+
});
124+
}
125+
}
126+
});
127+
VSS.notifyLoadSucceeded();
128+
});
129+
130+
function consoleLog(message) {
131+
console.log(message);
132+
}
133+
</script>
134+
135+
<div class="widget">
136+
<h2 class="title">Chart Widget</h2>
137+
<div id="Chart-Container"></div>
138+
</div>
139+
</body>
140+
</html>

chart/configuration_2x2.html

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<!DOCTYPE html>
2+
<html xmlns="http://www.w3.org/1999/xhtml">
3+
<head>
4+
<script src="../lib/VSS.SDK.min.js"></script>
5+
<link rel="stylesheet" href="../styles.css" />
6+
7+
<script type="text/javascript">
8+
VSS.init({
9+
explicitNotifyLoaded: true,
10+
usePlatformStyles: true
11+
});
12+
13+
VSS.require(["VSS/Service", "TFS/Dashboards/WidgetHelpers", "VSS/Context", "TFS/VersionControl/GitRestClient"],
14+
function (Service, WidgetHelpers, context, GitWebApi) {
15+
VSS.register("GHAzDoWidget.Chart.Configuration", function () {
16+
var $repoDropdown = $("#repo-dropdown");
17+
18+
async function getRepos() {
19+
try {
20+
const webContext = VSS.getWebContext();
21+
const project = webContext.project;
22+
23+
// todo: load the available repos in this project
24+
const gitClient = Service.getClient(GitWebApi.GitHttpClient);
25+
repos = await gitClient.getRepositories(project.name);
26+
console.log(`Found these repos: ${JSON.stringify(repos)}`);
27+
return repos;
28+
}
29+
catch (err) {
30+
console.log(`Error loading the available repos: ${err}`);
31+
return [];
32+
}
33+
}
34+
35+
return {
36+
load: async function (widgetSettings, widgetConfigurationContext) {
37+
var settings = JSON.parse(widgetSettings.customSettings.data);
38+
console.log(`Loading the Chart.2x2 settings with ${JSON.stringify(settings)}`)
39+
40+
const repos = await getRepos();
41+
// add all repos as selection options to the dropdown
42+
if (repos) {
43+
// sort the repo alphabetically
44+
repos.sort((a, b) => a.name.localeCompare(b.name));
45+
repos.forEach(r => {
46+
$repoDropdown.append(`<option value=${r.name}>${r.name}</option>`);
47+
});
48+
}
49+
50+
if (settings && settings.repo) {
51+
// select the repo that was saved in the settings
52+
$repoDropdown.val(settings.repo);
53+
}
54+
55+
$repoDropdown.on("change", function () {
56+
let repo;
57+
if (repos) {
58+
// find the repo with this name
59+
repo = repos.find(r => r.name === $repoDropdown.val());
60+
}
61+
62+
var customSettings = {
63+
data: JSON.stringify({
64+
repo: $repoDropdown.val(),
65+
repoId: repo.id
66+
})
67+
};
68+
var eventName = WidgetHelpers.WidgetEvent.ConfigurationChange;
69+
var eventArgs = WidgetHelpers.WidgetEvent.Args(customSettings);
70+
widgetConfigurationContext.notify(eventName, eventArgs);
71+
});
72+
73+
return WidgetHelpers.WidgetStatusHelper.Success();
74+
},
75+
onSave: async function() {
76+
const repos = await getRepos();
77+
let repo;
78+
if (repos) {
79+
// find the repo with this name
80+
repo = repos.find(r => r.name === $repoDropdown.val());
81+
}
82+
var customSettings = {
83+
data: JSON.stringify({
84+
repo: $repoDropdown.val(),
85+
repoId: repo.id
86+
})
87+
};
88+
console.log(`Saving the Chart.2x2 settings with ${JSON.stringify(customSettings)}`)
89+
return WidgetHelpers.WidgetConfigurationSave.Valid(customSettings);
90+
}
91+
}
92+
});
93+
VSS.notifyLoadSucceeded();
94+
});
95+
</script>
96+
</head>
97+
<body>
98+
<div class="container">
99+
<fieldset>
100+
<label class="label">Repository: </label>
101+
<select id="repo-dropdown" style="margin-top:10px">
102+
<!-- todo: dynamically load the available repos in this project-->
103+
</select>
104+
</fieldset>
105+
</div>
106+
</body>
107+
</html>

img/example_chart_2x2.png

73.3 KB
Loading

library.js

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ function authenticatedGet(url) {
2727
.then(x => x.json());
2828
}
2929

30-
async function getAlerts (organization, projectName, repoId) {
30+
async function getAlerts(organization, projectName, repoId) {
3131
consoleLog('getAlerts');
3232

3333
try{
34-
// todo: add pagination
34+
// no pagination option, so just get the first 5000 alerts
3535
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`;
3636
consoleLog(`Calling url: [${url}]`);
3737
const alertResult = await authenticatedGet(url);
@@ -51,4 +51,94 @@ async function getAlerts (organization, projectName, repoId) {
5151
catch (err) {
5252
consoleLog('error in calling the advec api: ' + err);
5353
}
54+
}
55+
56+
async function getAlertsTrendLines(organization, projectName, repoId) {
57+
consoleLog(`getAlertsTrend for organization [${organization}], project [${projectName}], repo [${repoId}]`);
58+
59+
try {
60+
url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/AdvancedSecurity/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=truen&api-version=7.2-preview.1`;
61+
consoleLog(`Calling url: [${url}]`);
62+
const alertResult = await authenticatedGet(url);
63+
//consoleLog('alertResult: ' + JSON.stringify(alertResult));
64+
consoleLog('alertResult count: ' + alertResult.count);
65+
66+
// load the Secret alerts and create a trend line over the last 3 weeks
67+
const secretAlerts = alertResult.value.filter(alert => alert.alertType === "secret");
68+
const secretAlertsTrend = getAlertsTrendLine(secretAlerts, 'secret');
69+
console.log('');
70+
// load the Dependency alerts and create a trend line over the last 3 weeks
71+
const dependencyAlerts = alertResult.value.filter(alert => alert.alertType === "dependency");
72+
const dependencyAlertsTrend = getAlertsTrendLine(dependencyAlerts, 'dependency');console.log('');
73+
console.log('');
74+
// load the Code alerts and create a trend line over the last 3 weeks
75+
const codeAlerts = alertResult.value.filter(alert => alert.alertType === "code");
76+
const codeAlertsTrend = getAlertsTrendLine(codeAlerts, 'code');
77+
78+
return {
79+
secretAlertsTrend: secretAlertsTrend,
80+
dependencyAlertsTrend: dependencyAlertsTrend,
81+
codeAlertsTrend: codeAlertsTrend
82+
};
83+
}
84+
catch (err) {
85+
consoleLog('error in calling the advec api: ' + err);
86+
}
87+
}
88+
89+
function checkAlertActiveOnDate(alert, dateStr) {
90+
// check if the alert.firstSeenDate is within the date range
91+
// and if fixedDate is not set or is after the date range
92+
const seenClosed = (alert.firstSeenDate.split('T')[0] <= dateStr && (!alert.fixedDate || alert.fixedDate.split('T')[0] > dateStr));
93+
if (seenClosed) {
94+
// check the dismissal.requestedOn date as well
95+
if (alert.dismissal && alert.dismissal.requestedOn) {
96+
const dismissed = (alert.dismissal.requestedOn.split('T')[0] <= dateStr);
97+
return !dismissed;
98+
}
99+
}
100+
101+
return seenClosed;
102+
}
103+
104+
function getAlertsTrendLine(alerts, type) {
105+
consoleLog(`getAlertsTrendLine for type ${type}`);
106+
107+
const trendLine = [];
108+
const trendLineSimple = [];
109+
const today = new Date();
110+
const threeWeeksAgo = new Date();
111+
threeWeeksAgo.setDate(today.getDate() - 21);
112+
113+
for (let d = threeWeeksAgo; d <= today; d.setDate(d.getDate() + 1)) {
114+
const date = new Date(d);
115+
const dateStr = date.toISOString().split('T')[0];
116+
117+
const alertsOnDate = alerts.filter(alert => checkAlertActiveOnDate(alert, dateStr));
118+
console.log(`On [${dateStr}] there were [${alertsOnDate.length}] active ${type} alerts`);
119+
trendLine.push({
120+
date: dateStr,
121+
count: alertsOnDate.length
122+
});
123+
124+
trendLineSimple.push(alertsOnDate.length);
125+
}
126+
127+
consoleLog('trendLine: ' + JSON.stringify(trendLineSimple));
128+
return trendLineSimple;
129+
}
130+
131+
function getDatePoints() {
132+
const trendDates = [];
133+
const today = new Date();
134+
const threeWeeksAgo = new Date();
135+
threeWeeksAgo.setDate(today.getDate() - 21);
136+
137+
for (let d = threeWeeksAgo; d <= today; d.setDate(d.getDate() + 1)) {
138+
const date = new Date(d);
139+
const dateStr = date.toISOString().split('T')[0];
140+
trendDates.push(dateStr);
141+
}
142+
143+
return trendDates;
54144
}

overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Install from the marketplace: https://marketplace.visualstudio.com/items?itemNam
1010
### Split it into three separate widgets (1 by 1) with just the single value you scan for:
1111
![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)
1212

13+
### Show a trend line (2 by 2) of all alerts in the last 3 weeks:
14+
![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)
15+
1316
## GitHub repo
1417
Please report issues, feature request, and feedback here: https://github.com/rajbos/GHAzDo-widget.
1518

0 commit comments

Comments
 (0)