Skip to content

Commit

Permalink
Add PR task to check for new alerts (#33)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rajbos authored Nov 29, 2023
1 parent d981d79 commit 9dd73ba
Show file tree
Hide file tree
Showing 14 changed files with 884 additions and 551 deletions.
82 changes: 82 additions & 0 deletions .github/workflows/handle-versioning-accross-branches.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Handle versioning accros branches

on:
push:
# todo: add file paths of the files with version numbers

jobs:
extension-versioning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: git-actions/set-user@v1

- name: Prevent branch warnings
run: |
# config git advice.detachedHead to false
git config advice.detachedHead false
- uses: actions/setup-node@v3
with:
node-version: 16

- name: Install tfx extension
run: |
npm install -g tfx-cli
- name: Get highest version number accross all branches
id: get-version
shell: pwsh
env:
AZURE_DEVOPS_CREATE_PAT: ${{ secrets.AZURE_DEVOPS_CREATE_PAT}}
run: |
# get the last updated version for this extension from the server
$output = $(tfx extension show --token $env:AZURE_DEVOPS_CREATE_PAT --vsix $vsix --publisher "RobBos" --extension-id "GHAzDoWidget-DEV" --output json | ConvertFrom-Json)
$lastVersion = ($output.versions | Sort-Object -Property lastUpdated -Descending)[0]
Write-Host "Last version: [$($lastVersion.version)] from server"
# SemVer code
function Parse-SemVer ($version) {
$parts = $version.Split('.')
return @{
Major = [int]$parts[0]
Minor = [int]$parts[1]
Patch = [int]$parts[2]
}
}
$highestVersion = @{
Major = 0
Minor = 0
Patch = 0
}
# loop over all branches
$highestVersion = 0
foreach ($branch in $(git branch -r --format='%(refname:short)')) {
Write-Host "Checkout the branch [$branch]"
git checkout $branch
# get the semantic version number from the version in the dependencyReviewTask/task.json file
$version = Get-Content -Path "dependencyReviewTask/task.json" | ConvertFrom-Json | Select-Object -ExpandProperty version
Write-Host "Found version: [$version] in branch: [$branch]"
# check if the version is semantically higher than the highest version using SemVer
if ($version.Major -gt $highestVersion.Major -or
($version.Major -eq $highestVersion.Major -and $version.Minor -gt $highestVersion.Minor) -or
($version.Major -eq $highestVersion.Major -and $version.Minor -eq $highestVersion.Minor -and $version.Patch -gt $highestVersion.Patch))
{
$highestVersion = $version
Write-Host "New highest version from PR task.json: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"
}
}
Write-Host "Highest version: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"
# show the highest version number in GitHub by writing to the job summary file
Set-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Highest version of the extension: [$($lastVersion.version)]"
Set-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Highest version of the PR check extension: [$($highestVersion.Major).$($highestVersion.Minor).$($highestVersion.Patch)]"
206 changes: 135 additions & 71 deletions dependencyReviewTask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,35 @@ interface IResponse {
result: IResult;
}

async function getAlerts(connection: WebApi, orgSlug: string, project: string, repository: string, branchName: string) {
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`;
async function getAlerts(
connection: WebApi,
orgSlug: string,
project: string,
repository: string,
branchName: string,
alertType: number
)
{
if (!(alertType == 1 || alertType == 3)) {
console.log(`Error loading alerts for branch [${branchName}] with unknown alertType [${alertType}]`)
return null
}

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`
tl.debug(`Calling api with url: [${branchUrl}]`)

let branchResponse: IResponse

try {
branchResponse = await connection.rest.get<IResult>(branchUrl);
branchResponse = await connection.rest.get<IResult>(branchUrl)
}
catch (err: unknown) {
if (err instanceof Error) {
if (err.message.includes('Branch does not exist')) {
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).`);
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).`)
}
else {
console.log(`An error occurred: ${err.message}`);
console.log(`An error occurred: ${err.message}`)
}
}
}
Expand All @@ -49,87 +64,136 @@ async function getAlerts(connection: WebApi, orgSlug: string, project: string, r
async function run() {
try {
// test to see if this build was triggered with a PR context
const buildReason = tl.getVariable('Build.Reason');
const buildReason = tl.getVariable('Build.Reason')
if (buildReason != 'PullRequest') {
tl.setResult(tl.TaskResult.Skipped, `This extension only works when triggered by a Pull Request and not by a [${buildReason}]`);
tl.setResult(tl.TaskResult.Skipped, `This extension only works when triggered by a Pull Request and not by a [${buildReason}]`)
return
}

// todo: convert to some actual setting
const inputString: string | undefined = tl.getInput('samplestring', true);
if (inputString == 'bad') {
tl.setResult(tl.TaskResult.Failed, 'Bad input was given');

// stop the task execution
return;
// todo: convert to some actual value | boolean setting, for example severity score or switch between Dependency and CodeQL alerts
const scanForDependencyAlerts : string | undefined = tl.getInput('DepedencyAlertsScan', true)
tl.debug(`scanForDependencyAlerts setting value: ${scanForDependencyAlerts}`)

const scanForCodeScanningAlerts : string | undefined = tl.getInput('CodeScanningAlerts', true)
tl.debug(`scanForCodeScanningAlerts setting value: ${scanForCodeScanningAlerts}`)

const token = getSystemAccessToken()
const authHandler = getHandlerFromToken(token)
const uri = tl.getVariable("System.CollectionUri")
const connection = new WebApi(uri, authHandler)

const organization = tl.getVariable('System.TeamFoundationCollectionUri')
const orgSlug = organization.split('/')[3]
const project = tl.getVariable('System.TeamProject')
const repository = tl.getVariable('Build.Repository.ID')
const sourceBranch = tl.getVariable('System.PullRequest.SourceBranch')
const sourceBranchName = sourceBranch?.split('/')[2]
const targetBranchName = tl.getVariable('System.PullRequest.targetBranchName')

let alertType = 0
let errorString = ""
console.log(`Retrieving alerts with token: [${token}], organization: [${organization}], orgSlug: [${orgSlug}], project: [${project}], sourceBranchName: [${sourceBranchName}], targetBranchName: [${targetBranchName}]`)
if (scanForDependencyAlerts == 'true') {
alertType = 1 // Dependency Scanning alerts
const dependencyResult = await checkAlertsForType(connection, orgSlug, project, repository, alertType, sourceBranchName, targetBranchName)
if (dependencyResult.newAlertsFound) {
errorString += dependencyResult.message
}
}
console.log('Hello', inputString);

const token = getSystemAccessToken();
const authHandler = getHandlerFromToken(token);
const uri = tl.getVariable("System.CollectionUri");
const connection = new WebApi(uri, authHandler);
if (scanForCodeScanningAlerts == 'true') {
alertType = 3 // Code Scanning alerts
const codeScanningResult = await checkAlertsForType(connection, orgSlug, project, repository, alertType, sourceBranchName, targetBranchName)
if (codeScanningResult.newAlertsFound) {
errorString += codeScanningResult.message
}
}

const organization = tl.getVariable('System.TeamFoundationCollectionUri');
const orgSlug = organization.split('/')[3];
const project = tl.getVariable('System.TeamProject');
const repository = tl.getVariable('Build.Repository.ID');
const sourceBranch = tl.getVariable('System.PullRequest.SourceBranch');
const sourceBranchName = sourceBranch?.split('/')[2];
const targetBranchName = tl.getVariable('System.PullRequest.targetBranchName');
if (scanForDependencyAlerts !== 'true' && scanForCodeScanningAlerts !== 'true') {
const message = `No options selected to check for either dependency scanning alerts or code scanning alerts`
console.log(message)
tl.setResult(tl.TaskResult.Skipped, message)
return
}

console.log(`Retrieving alerts with token: [${token}], organization: [${organization}], orgSlug: [${orgSlug}], project: [${project}], sourceBranchName: [${sourceBranchName}], targetBranchName: [${targetBranchName}]`);
if (errorString.length > 0) {
tl.setResult(tl.TaskResult.Failed, errorString)
}
}
catch (err: unknown) {
if (err instanceof Error) {
tl.setResult(tl.TaskResult.Failed, err.message)
} else {
tl.setResult(tl.TaskResult.Failed, 'An unknown error occurred')
}
}

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

tl.debug(`source response: ${JSON.stringify(sourceBranchResponse)}`);
tl.debug(`target response: ${JSON.stringify(targetBranchResponse)}`);
async function checkAlertsForType(
connection: WebApi,
orgSlug: string,
project: string,
repository: string,
alertType: number,
sourceBranchName: string,
targetBranchName: string
): Promise<{newAlertsFound: boolean, message: string}>
{
const sourceBranchResponse = await getAlerts(connection, orgSlug, project, repository, sourceBranchName, alertType)
const targetBranchResponse = await getAlerts(connection, orgSlug, project, repository, targetBranchName, alertType)

// todo: check if response.statuscode === 404 and skip the rest, do report a warning
tl.debug(`source response: ${JSON.stringify(sourceBranchResponse)}`)
tl.debug(`target response: ${JSON.stringify(targetBranchResponse)}`)

let alertTypeString = `Dependency`
if (alertType == 3) {
alertTypeString = `Code scanning`
}

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

tl.setResult(tl.TaskResult.Succeeded, `Found no alerts for the source branch`);
return;
}
else {
// check by result.alertId if there is a new alert or not (so alert not in targetBranch)

// first get the only the alertid's from the source branch
const sourceAlertIds = sourceBranchResponse.result.value.map((alert) => {return alert.alertId;});
// do the same for the target branch
const targetAlertIds = targetBranchResponse.result.value.map((alert) => {return alert.alertId;});
// now find the delta
const newAlertIds = sourceAlertIds.filter((alertId) => {
return !targetAlertIds.includes(alertId);
});

if (newAlertIds.length > 0) {

console.log(`Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] of which [${newAlertIds.length}] are new:`);
for (const alertId of newAlertIds) {
// get the alert details:
const alertUrl = `https://dev.azure.com/${orgSlug}/${project}/_git/${repository}/alerts/${alertId}?branch=refs/heads/${sourceBranchName}`;
const alertTitle = sourceBranchResponse.result.value.find((alert) => {return alert.alertId == alertId;})?.title;
// and show them:
console.log(`- ${alertId}: ${alertTitle}, url: ${alertUrl}`);
}

tl.setResult(tl.TaskResult.Failed, `Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] of which [${newAlertIds.length}] are new`);
}
else {
console.log(`Found no new alerts for the source branch [${sourceBranchName}]`);
tl.setResult(tl.TaskResult.Succeeded, `Found no new alerts for the source branch [${sourceBranchName}], only [${targetBranchResponse.result.count}] existing ones`);
//tl.setResult(tl.TaskResult.Succeeded, `Found no alerts for the source branch`)
return { newAlertsFound: false, message: `` }
}
else {
// check by result.alertId if there is a new alert or not (so alert not in targetBranch)

// first get the only the alertid's from the source branch
const sourceAlertIds = sourceBranchResponse.result.value.map((alert) => {return alert.alertId;})
// do the same for the target branch
const targetAlertIds = targetBranchResponse.result.value.map((alert) => {return alert.alertId;})
// now find the delta
const newAlertIds = sourceAlertIds.filter((alertId) => {
return !targetAlertIds.includes(alertId)
});

if (newAlertIds.length > 0) {
let message =`Found [${sourceBranchResponse.result.count}] alerts for the source branch [${sourceBranchName}] for alert type [${alertTypeString}] of which [${newAlertIds.length}] are new:`
console.log(message)
for (const alertId of newAlertIds) {
// get the alert details:
const alertUrl = `https://dev.azure.com/${orgSlug}/${project.replace(" ", "%20")}/_git/${repository}/alerts/${alertId}?branch=refs/heads/${sourceBranchName}`
const alertTitle = sourceBranchResponse.result.value.find((alert) => {return alert.alertId == alertId;})?.title
// and show them:
const specificAlertMessage = `- ${alertId}: ${alertTitle}, url: ${alertUrl}`
console.log(specificAlertMessage)
message += `\r\n${specificAlertMessage}` // todo: check if this new line actually works :-)
// tested \\n --> did not work
// tested \\r\\n --> did not work
}
return {newAlertsFound: true, message: message}
}
}
catch (err: unknown) {
if (err instanceof Error) {
tl.setResult(tl.TaskResult.Failed, err.message);
} else {
tl.setResult(tl.TaskResult.Failed, 'An unknown error occurred');
else {
const message = `Found no new alerts for the source branch [${sourceBranchName}] for alert type [${alertTypeString}]`
console.log(message)
return {newAlertsFound: false, message: message}
}
}
}

run();
run()
28 changes: 18 additions & 10 deletions dependencyReviewTask/task.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
{
"$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
"id": "10c1d88a-9d0f-4288-8e37-58762caa0b8b",
"name": "Advanced-Dependency-Review",
"friendlyName": "Advanced Security Dependency Review",
"description": "Scan the source branch in your PR for known Dependency issues",
"helpMarkDown": "Checks the source branch in your PR for known Dependency issues",
"name": "Advanced-Security-Review",
"friendlyName": "Advanced Security Review",
"description": "Scan the source branch in your PR for known Advanced Security issues",
"helpMarkDown": "Checks the source branch in your PR for known Advanced Security issues",
"category": "Utility",
"author": "RobBos",
"version": {
"Major": 0,
"Minor": 1,
"Patch": 23
"Patch": 37
},
"instanceNameFormat": "Echo $(samplestring)",
"inputs": [
{
"name": "samplestring",
"type": "string",
"label": "Sample String",
"defaultValue": "",
"name": "DepedencyAlertsScan",
"type": "boolean",
"label": "Fail on new dependency alerts",
"defaultValue": true,
"required": true,
"helpMarkDown": "A sample string"
"helpMarkDown": "Fail the pipeline if there is a new dependency alert"
},
{
"name": "CodeScanningAlerts",
"type": "boolean",
"label": "Fail on new code scanning alerts",
"defaultValue": true,
"required": true,
"helpMarkDown": "Fail the pipeline if there is a new code scanning alert"
}
],
"execution": {
Expand Down
Binary file added img/dependencyReviewTask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 9dd73ba

Please sign in to comment.