Skip to content

Commit 9dd73ba

Browse files
authored
Add PR task to check for new alerts (#33)
* PR-task: support multiple AlertTypes to check * Add config and load option for "all repos in project" * Determining the highst version in the extension file * fix error * Load main version from AzDo server * fix pwsh code * install the tfx extension * check if we hit all branches * get all branches * Updated the output * fix output * Map URLS to new area name * updates * Updated advanced security review task * temp checkin * update extension with PR task support - release to PROD
1 parent d981d79 commit 9dd73ba

14 files changed

+884
-551
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Handle versioning accros branches
2+
3+
on:
4+
push:
5+
# todo: add file paths of the files with version numbers
6+
7+
jobs:
8+
extension-versioning:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
fetch-depth: 0
14+
15+
- uses: git-actions/set-user@v1
16+
17+
- name: Prevent branch warnings
18+
run: |
19+
# config git advice.detachedHead to false
20+
git config advice.detachedHead false
21+
22+
- uses: actions/setup-node@v3
23+
with:
24+
node-version: 16
25+
26+
- name: Install tfx extension
27+
run: |
28+
npm install -g tfx-cli
29+
30+
- name: Get highest version number accross all branches
31+
id: get-version
32+
shell: pwsh
33+
env:
34+
AZURE_DEVOPS_CREATE_PAT: ${{ secrets.AZURE_DEVOPS_CREATE_PAT}}
35+
run: |
36+
# get the last updated version for this extension from the server
37+
$output = $(tfx extension show --token $env:AZURE_DEVOPS_CREATE_PAT --vsix $vsix --publisher "RobBos" --extension-id "GHAzDoWidget-DEV" --output json | ConvertFrom-Json)
38+
$lastVersion = ($output.versions | Sort-Object -Property lastUpdated -Descending)[0]
39+
Write-Host "Last version: [$($lastVersion.version)] from server"
40+
41+
# SemVer code
42+
function Parse-SemVer ($version) {
43+
$parts = $version.Split('.')
44+
return @{
45+
Major = [int]$parts[0]
46+
Minor = [int]$parts[1]
47+
Patch = [int]$parts[2]
48+
}
49+
}
50+
51+
$highestVersion = @{
52+
Major = 0
53+
Minor = 0
54+
Patch = 0
55+
}
56+
57+
# loop over all branches
58+
$highestVersion = 0
59+
foreach ($branch in $(git branch -r --format='%(refname:short)')) {
60+
Write-Host "Checkout the branch [$branch]"
61+
git checkout $branch
62+
63+
# get the semantic version number from the version in the dependencyReviewTask/task.json file
64+
$version = Get-Content -Path "dependencyReviewTask/task.json" | ConvertFrom-Json | Select-Object -ExpandProperty version
65+
Write-Host "Found version: [$version] in branch: [$branch]"
66+
67+
# check if the version is semantically higher than the highest version using SemVer
68+
if ($version.Major -gt $highestVersion.Major -or
69+
($version.Major -eq $highestVersion.Major -and $version.Minor -gt $highestVersion.Minor) -or
70+
($version.Major -eq $highestVersion.Major -and $version.Minor -eq $highestVersion.Minor -and $version.Patch -gt $highestVersion.Patch))
71+
{
72+
$highestVersion = $version
73+
74+
Write-Host "New highest version from PR task.json: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"
75+
}
76+
}
77+
78+
Write-Host "Highest version: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"
79+
80+
# show the highest version number in GitHub by writing to the job summary file
81+
Set-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Highest version of the extension: [$($lastVersion.version)]"
82+
Set-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Highest version of the PR check extension: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"

dependencyReviewTask/index.ts

+135-71
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,35 @@ interface IResponse {
2626
result: IResult;
2727
}
2828

29-
async function getAlerts(connection: WebApi, orgSlug: string, project: string, repository: string, branchName: string) {
30-
const branchUrl = `https://advsec.dev.azure.com/${orgSlug}/${project}/_apis/AdvancedSecurity/repositories/${repository}/alerts?criteria.alertType=1&criteria.ref=${branchName}&criteria.onlyDefaultBranchAlerts=true&useDatabaseProvider=true`;
29+
async function getAlerts(
30+
connection: WebApi,
31+
orgSlug: string,
32+
project: string,
33+
repository: string,
34+
branchName: string,
35+
alertType: number
36+
)
37+
{
38+
if (!(alertType == 1 || alertType == 3)) {
39+
console.log(`Error loading alerts for branch [${branchName}] with unknown alertType [${alertType}]`)
40+
return null
41+
}
42+
43+
const branchUrl = `https://advsec.dev.azure.com/${orgSlug}/${project.replace(" ", "%20")}/_apis/alert/repositories/${repository}/alerts?criteria.alertType=${alertType}&criteria.ref=${branchName}&criteria.onlyDefaultBranchAlerts=true&useDatabaseProvider=true`
44+
tl.debug(`Calling api with url: [${branchUrl}]`)
45+
3146
let branchResponse: IResponse
3247

3348
try {
34-
branchResponse = await connection.rest.get<IResult>(branchUrl);
49+
branchResponse = await connection.rest.get<IResult>(branchUrl)
3550
}
3651
catch (err: unknown) {
3752
if (err instanceof Error) {
3853
if (err.message.includes('Branch does not exist')) {
39-
console.log(`Branch [${branchName}] does not exist in GHAzDo yet. Make sure to run the Dependency Scan task first on this branch (easiest to do in the same pipeline).`);
54+
console.log(`Branch [${branchName}] does not exist in GHAzDo yet. Make sure to run the Dependency Scan task first on this branch (easiest to do in the same pipeline).`)
4055
}
4156
else {
42-
console.log(`An error occurred: ${err.message}`);
57+
console.log(`An error occurred: ${err.message}`)
4358
}
4459
}
4560
}
@@ -49,87 +64,136 @@ async function getAlerts(connection: WebApi, orgSlug: string, project: string, r
4964
async function run() {
5065
try {
5166
// test to see if this build was triggered with a PR context
52-
const buildReason = tl.getVariable('Build.Reason');
67+
const buildReason = tl.getVariable('Build.Reason')
5368
if (buildReason != 'PullRequest') {
54-
tl.setResult(tl.TaskResult.Skipped, `This extension only works when triggered by a Pull Request and not by a [${buildReason}]`);
69+
tl.setResult(tl.TaskResult.Skipped, `This extension only works when triggered by a Pull Request and not by a [${buildReason}]`)
5570
return
5671
}
5772

58-
// todo: convert to some actual setting
59-
const inputString: string | undefined = tl.getInput('samplestring', true);
60-
if (inputString == 'bad') {
61-
tl.setResult(tl.TaskResult.Failed, 'Bad input was given');
62-
63-
// stop the task execution
64-
return;
73+
// todo: convert to some actual value | boolean setting, for example severity score or switch between Dependency and CodeQL alerts
74+
const scanForDependencyAlerts : string | undefined = tl.getInput('DepedencyAlertsScan', true)
75+
tl.debug(`scanForDependencyAlerts setting value: ${scanForDependencyAlerts}`)
76+
77+
const scanForCodeScanningAlerts : string | undefined = tl.getInput('CodeScanningAlerts', true)
78+
tl.debug(`scanForCodeScanningAlerts setting value: ${scanForCodeScanningAlerts}`)
79+
80+
const token = getSystemAccessToken()
81+
const authHandler = getHandlerFromToken(token)
82+
const uri = tl.getVariable("System.CollectionUri")
83+
const connection = new WebApi(uri, authHandler)
84+
85+
const organization = tl.getVariable('System.TeamFoundationCollectionUri')
86+
const orgSlug = organization.split('/')[3]
87+
const project = tl.getVariable('System.TeamProject')
88+
const repository = tl.getVariable('Build.Repository.ID')
89+
const sourceBranch = tl.getVariable('System.PullRequest.SourceBranch')
90+
const sourceBranchName = sourceBranch?.split('/')[2]
91+
const targetBranchName = tl.getVariable('System.PullRequest.targetBranchName')
92+
93+
let alertType = 0
94+
let errorString = ""
95+
console.log(`Retrieving alerts with token: [${token}], organization: [${organization}], orgSlug: [${orgSlug}], project: [${project}], sourceBranchName: [${sourceBranchName}], targetBranchName: [${targetBranchName}]`)
96+
if (scanForDependencyAlerts == 'true') {
97+
alertType = 1 // Dependency Scanning alerts
98+
const dependencyResult = await checkAlertsForType(connection, orgSlug, project, repository, alertType, sourceBranchName, targetBranchName)
99+
if (dependencyResult.newAlertsFound) {
100+
errorString += dependencyResult.message
101+
}
65102
}
66-
console.log('Hello', inputString);
67103

68-
const token = getSystemAccessToken();
69-
const authHandler = getHandlerFromToken(token);
70-
const uri = tl.getVariable("System.CollectionUri");
71-
const connection = new WebApi(uri, authHandler);
104+
if (scanForCodeScanningAlerts == 'true') {
105+
alertType = 3 // Code Scanning alerts
106+
const codeScanningResult = await checkAlertsForType(connection, orgSlug, project, repository, alertType, sourceBranchName, targetBranchName)
107+
if (codeScanningResult.newAlertsFound) {
108+
errorString += codeScanningResult.message
109+
}
110+
}
72111

73-
const organization = tl.getVariable('System.TeamFoundationCollectionUri');
74-
const orgSlug = organization.split('/')[3];
75-
const project = tl.getVariable('System.TeamProject');
76-
const repository = tl.getVariable('Build.Repository.ID');
77-
const sourceBranch = tl.getVariable('System.PullRequest.SourceBranch');
78-
const sourceBranchName = sourceBranch?.split('/')[2];
79-
const targetBranchName = tl.getVariable('System.PullRequest.targetBranchName');
112+
if (scanForDependencyAlerts !== 'true' && scanForCodeScanningAlerts !== 'true') {
113+
const message = `No options selected to check for either dependency scanning alerts or code scanning alerts`
114+
console.log(message)
115+
tl.setResult(tl.TaskResult.Skipped, message)
116+
return
117+
}
80118

81-
console.log(`Retrieving alerts with token: [${token}], organization: [${organization}], orgSlug: [${orgSlug}], project: [${project}], sourceBranchName: [${sourceBranchName}], targetBranchName: [${targetBranchName}]`);
119+
if (errorString.length > 0) {
120+
tl.setResult(tl.TaskResult.Failed, errorString)
121+
}
122+
}
123+
catch (err: unknown) {
124+
if (err instanceof Error) {
125+
tl.setResult(tl.TaskResult.Failed, err.message)
126+
} else {
127+
tl.setResult(tl.TaskResult.Failed, 'An unknown error occurred')
128+
}
129+
}
82130

83-
const sourceBranchResponse = await getAlerts(connection, orgSlug, project, repository, sourceBranchName);
84-
const targetBranchResponse = await getAlerts(connection, orgSlug, project, repository, targetBranchName);
131+
// everything worked, no new alerts found and at least one scanning option was enabled
132+
tl.setResult(tl.TaskResult.Succeeded)
133+
}
85134

86-
tl.debug(`source response: ${JSON.stringify(sourceBranchResponse)}`);
87-
tl.debug(`target response: ${JSON.stringify(targetBranchResponse)}`);
135+
async function checkAlertsForType(
136+
connection: WebApi,
137+
orgSlug: string,
138+
project: string,
139+
repository: string,
140+
alertType: number,
141+
sourceBranchName: string,
142+
targetBranchName: string
143+
): Promise<{newAlertsFound: boolean, message: string}>
144+
{
145+
const sourceBranchResponse = await getAlerts(connection, orgSlug, project, repository, sourceBranchName, alertType)
146+
const targetBranchResponse = await getAlerts(connection, orgSlug, project, repository, targetBranchName, alertType)
147+
148+
// todo: check if response.statuscode === 404 and skip the rest, do report a warning
149+
tl.debug(`source response: ${JSON.stringify(sourceBranchResponse)}`)
150+
tl.debug(`target response: ${JSON.stringify(targetBranchResponse)}`)
151+
152+
let alertTypeString = `Dependency`
153+
if (alertType == 3) {
154+
alertTypeString = `Code scanning`
155+
}
88156

89-
if (sourceBranchResponse.result.count == 0) {
90-
console.log('No alerts found for this branch');
157+
if (!sourceBranchResponse || sourceBranchResponse?.result?.count == 0) {
158+
console.log(`No alerts found for this branch [${sourceBranchName}] for alert type [${alertTypeString}]`)
91159

92-
tl.setResult(tl.TaskResult.Succeeded, `Found no alerts for the source branch`);
93-
return;
94-
}
95-
else {
96-
// check by result.alertId if there is a new alert or not (so alert not in targetBranch)
97-
98-
// first get the only the alertid's from the source branch
99-
const sourceAlertIds = sourceBranchResponse.result.value.map((alert) => {return alert.alertId;});
100-
// do the same for the target branch
101-
const targetAlertIds = targetBranchResponse.result.value.map((alert) => {return alert.alertId;});
102-
// now find the delta
103-
const newAlertIds = sourceAlertIds.filter((alertId) => {
104-
return !targetAlertIds.includes(alertId);
105-
});
106-
107-
if (newAlertIds.length > 0) {
108-
109-
console.log(`Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] of which [${newAlertIds.length}] are new:`);
110-
for (const alertId of newAlertIds) {
111-
// get the alert details:
112-
const alertUrl = `https://dev.azure.com/${orgSlug}/${project}/_git/${repository}/alerts/${alertId}?branch=refs/heads/${sourceBranchName}`;
113-
const alertTitle = sourceBranchResponse.result.value.find((alert) => {return alert.alertId == alertId;})?.title;
114-
// and show them:
115-
console.log(`- ${alertId}: ${alertTitle}, url: ${alertUrl}`);
116-
}
117-
118-
tl.setResult(tl.TaskResult.Failed, `Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] of which [${newAlertIds.length}] are new`);
119-
}
120-
else {
121-
console.log(`Found no new alerts for the source branch [${sourceBranchName}]`);
122-
tl.setResult(tl.TaskResult.Succeeded, `Found no new alerts for the source branch [${sourceBranchName}], only [${targetBranchResponse.result.count}] existing ones`);
160+
//tl.setResult(tl.TaskResult.Succeeded, `Found no alerts for the source branch`)
161+
return { newAlertsFound: false, message: `` }
162+
}
163+
else {
164+
// check by result.alertId if there is a new alert or not (so alert not in targetBranch)
165+
166+
// first get the only the alertid's from the source branch
167+
const sourceAlertIds = sourceBranchResponse.result.value.map((alert) => {return alert.alertId;})
168+
// do the same for the target branch
169+
const targetAlertIds = targetBranchResponse.result.value.map((alert) => {return alert.alertId;})
170+
// now find the delta
171+
const newAlertIds = sourceAlertIds.filter((alertId) => {
172+
return !targetAlertIds.includes(alertId)
173+
});
174+
175+
if (newAlertIds.length > 0) {
176+
let message =`Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] for alert type [${alertTypeString}] of which [${newAlertIds.length}] are new:`
177+
console.log(message)
178+
for (const alertId of newAlertIds) {
179+
// get the alert details:
180+
const alertUrl = `https://dev.azure.com/${orgSlug}/${project.replace(" ", "%20")}/_git/${repository}/alerts/${alertId}?branch=refs/heads/${sourceBranchName}`
181+
const alertTitle = sourceBranchResponse.result.value.find((alert) => {return alert.alertId == alertId;})?.title
182+
// and show them:
183+
const specificAlertMessage = `- ${alertId}: ${alertTitle}, url: ${alertUrl}`
184+
console.log(specificAlertMessage)
185+
message += `\r\n${specificAlertMessage}` // todo: check if this new line actually works :-)
186+
// tested \\n --> did not work
187+
// tested \\r\\n --> did not work
123188
}
189+
return {newAlertsFound: true, message: message}
124190
}
125-
}
126-
catch (err: unknown) {
127-
if (err instanceof Error) {
128-
tl.setResult(tl.TaskResult.Failed, err.message);
129-
} else {
130-
tl.setResult(tl.TaskResult.Failed, 'An unknown error occurred');
191+
else {
192+
const message = `Found no new alerts for the source branch [${sourceBranchName}] for alert type [${alertTypeString}]`
193+
console.log(message)
194+
return {newAlertsFound: false, message: message}
131195
}
132196
}
133197
}
134198

135-
run();
199+
run()

dependencyReviewTask/task.json

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
33
"id": "10c1d88a-9d0f-4288-8e37-58762caa0b8b",
4-
"name": "Advanced-Dependency-Review",
5-
"friendlyName": "Advanced Security Dependency Review",
6-
"description": "Scan the source branch in your PR for known Dependency issues",
7-
"helpMarkDown": "Checks the source branch in your PR for known Dependency issues",
4+
"name": "Advanced-Security-Review",
5+
"friendlyName": "Advanced Security Review",
6+
"description": "Scan the source branch in your PR for known Advanced Security issues",
7+
"helpMarkDown": "Checks the source branch in your PR for known Advanced Security issues",
88
"category": "Utility",
99
"author": "RobBos",
1010
"version": {
1111
"Major": 0,
1212
"Minor": 1,
13-
"Patch": 23
13+
"Patch": 37
1414
},
1515
"instanceNameFormat": "Echo $(samplestring)",
1616
"inputs": [
1717
{
18-
"name": "samplestring",
19-
"type": "string",
20-
"label": "Sample String",
21-
"defaultValue": "",
18+
"name": "DepedencyAlertsScan",
19+
"type": "boolean",
20+
"label": "Fail on new dependency alerts",
21+
"defaultValue": true,
2222
"required": true,
23-
"helpMarkDown": "A sample string"
23+
"helpMarkDown": "Fail the pipeline if there is a new dependency alert"
24+
},
25+
{
26+
"name": "CodeScanningAlerts",
27+
"type": "boolean",
28+
"label": "Fail on new code scanning alerts",
29+
"defaultValue": true,
30+
"required": true,
31+
"helpMarkDown": "Fail the pipeline if there is a new code scanning alert"
2432
}
2533
],
2634
"execution": {

img/dependencyReviewTask.png

61.5 KB
Loading

0 commit comments

Comments
 (0)