From 9dd73ba53abee0f8dd4777bf63c169a2afe14689 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Thu, 30 Nov 2023 00:48:21 +0100 Subject: [PATCH] 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 --- .../handle-versioning-accross-branches.yml | 82 +++ dependencyReviewTask/index.ts | 206 ++++--- dependencyReviewTask/task.json | 28 +- img/dependencyReviewTask.png | Bin 0 -> 63022 bytes make.ps1 | 53 +- overview.md | 19 +- vss-extension-dev.json | 542 +++++++++--------- vss-extension.json | 372 ++++++------ widgets/library.js | 49 +- .../widgets/widget_1x1/configuration_1x1.html | 13 +- widgets/widgets/widget_1x1/widget_1x1.html | 17 +- widgets/widgets/widget_1x1/widget_1x1.js | 45 +- widgets/widgets/widget_2x1/widget_2x1.html | 2 +- z-status.txt | 7 + 14 files changed, 884 insertions(+), 551 deletions(-) create mode 100644 .github/workflows/handle-versioning-accross-branches.yml create mode 100644 img/dependencyReviewTask.png create mode 100644 z-status.txt diff --git a/.github/workflows/handle-versioning-accross-branches.yml b/.github/workflows/handle-versioning-accross-branches.yml new file mode 100644 index 0000000..6ef5cfa --- /dev/null +++ b/.github/workflows/handle-versioning-accross-branches.yml @@ -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)]" diff --git a/dependencyReviewTask/index.ts b/dependencyReviewTask/index.ts index d08c391..256cbac 100644 --- a/dependencyReviewTask/index.ts +++ b/dependencyReviewTask/index.ts @@ -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(branchUrl); + branchResponse = await connection.rest.get(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}`) } } } @@ -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(); \ No newline at end of file +run() \ No newline at end of file diff --git a/dependencyReviewTask/task.json b/dependencyReviewTask/task.json index 3f59ef9..204ab8c 100644 --- a/dependencyReviewTask/task.json +++ b/dependencyReviewTask/task.json @@ -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": { diff --git a/img/dependencyReviewTask.png b/img/dependencyReviewTask.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd6b0e041fcf38b365f7e1882eda378189b9722 GIT binary patch literal 63022 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 HuRr`BQFt^_ literal 0 HcmV?d00001 diff --git a/make.ps1 b/make.ps1 index fb6a0ff..1752268 100644 --- a/make.ps1 +++ b/make.ps1 @@ -1,6 +1,6 @@ # load the given arguments param( - [ValidateSet("provision", "build", "")] + [ValidateSet("provision", "build", "publish", "")] [string]$command = "provision", [int] $provisionCount = 100 ) @@ -263,8 +263,12 @@ function New-VSTSAuthenticationToken { return $accesstoken; } -if ("build" -eq $command) { - Write-Host "Building the dev version" +function Build { + param ( + [ValidateSet("dev", "prod")] + [string] $buildtype + ) + Write-Host "Building the [$buildtype] version" # check if $env:AZURE_DEVOPS_PAT has a value if ($null -eq $env:AZURE_DEVOPS_PAT) { @@ -272,11 +276,36 @@ if ("build" -eq $command) { exit } - # run the default: build the dev version + # default values for dev + $vsix = "vss-extension-dev.json" $extensionPrefix="RobBos.GHAzDoWidget-DEV-" - # delete all files with the name RobBos.GHAzDoWidget-DEV*.vsix + $extensionId = "GHAzDoWidget-DEV" + + if ($buildtype -eq "prod") { + # values for prod + $vsix = "vss-extension.json" + $extensionPrefix="RobBos.GHAzDoWidget-" + $extensionId = "GHAzDoWidget" + } + + # delete all files with the name prefix*.vsix Get-ChildItem -Path .\ -Filter $extensionPrefix*.vsix | Remove-Item -Force + # get the last updated version for this extension from the server to make sure we are rolling forward + try { + $output = $(tfx extension show --token $env:AZURE_DEVOPS_PAT --vsix $vsix --publisher "RobBos" --extension-id $extensionId --output json | ConvertFrom-Json) + $lastVersion = ($output.versions | Sort-Object -Property lastUpdated -Descending)[0] + Write-Host "Last version: [$($lastVersion.version)] from server" + # overwrite the version in the json file + $json = Get-Content .\$vsix | ConvertFrom-Json -Depth 10 + $json.version = $lastVersion.version + # write the json file back + $json | ConvertTo-Json -Depth 10 | Set-Content .\$vsix + } + catch { + Write-Host "Error loading the version from Azure DevOps Marketplace" + } + # build the task # todo: up the version number Set-Location .\dependencyReviewTask @@ -286,15 +315,23 @@ if ("build" -eq $command) { Set-Location .. # package the whole extension - tfx extension create --manifest-globs vss-extension-dev.json --rev-version + tfx extension create --manifest-globs $vsix --rev-version # get the new version number from the json file - $json = Get-Content .\vss-extension-dev.json | ConvertFrom-Json + $json = Get-Content .\$vsix | ConvertFrom-Json $visx = "$extensionPrefix$($json.version).vsix" Write-Host "Publishing [$visx]" - tfx extension publish --vsix $visx --service-url https://marketplace.visualstudio.com --token "$($env:AZURE_DEVOPS_PAT)" + tfx extension publish --vsix $visx --service-url https://marketplace.visualstudio.com --token "$($env:AZURE_DEVOPS_PAT)" +} + +if ("build" -eq $command) { + Build -buildtype "dev" + exit +} +if ("publish" -eq $command) { + Build -buildtype "prod" exit } diff --git a/overview.md b/overview.md index f79020a..f053cba 100644 --- a/overview.md +++ b/overview.md @@ -1,4 +1,4 @@ -Extension for Azure DevOps that shows the number of open security alerts for the configured repository. Please install it and let me know what you think! Create an issue for feedback or feature requests. +Extension for Azure DevOps that shows the number of open security alerts from GitHub Advanced Security for Azure DevOps, for the configured repository. Please install it and let me know what you think! Create an issue for feedback or feature requests. Install from the marketplace: https://marketplace.visualstudio.com/items?itemName=RobBos.GHAzDoWidget @@ -6,10 +6,21 @@ Install from the marketplace: https://marketplace.visualstudio.com/items?itemNam * Show all three alert counts in one widget in 2 by 1 layout * Split it into three separate widgets with just the single value you scan for (1x1 or 2x1) * Show a trend line of all alerts in the last 3 weeks (2x2,3x2,4x2) +* Show a pie chart with the distribution of alerts based on the severity level (2x2,3x2,4x2) -![Screenshot of the all the widgets with alert count for dependencies, secrets, and code scanning](/img/overview_600.png) +![Screenshot of the all the widgets with alert count for dependencies, secrets, and code scanning](/img/overview_600.png) + +> Note: only project level dashboards are supported at the moment. + +## Pipeline tasks +* Advanced-Security-Review: lets you check the pull request for newly introduced alerts from Dependency Scanning or Code Scanning (configurable). If new alerts are introduced, the task will fail. +> Note: +> * Needs to run in a PR context, or it will be skipped. +> * The DependencyScanTask needs to have run on the source branch of the PR **before the PR check runs** + +### Advanced Security Review Output +If new alerts are found in the source branch (compared to the target branch), the task will fail: +![Screenshot of the failure message of the review task](/img/dependencyReviewTask.png) ## GitHub repo Please report issues, feature request, and feedback here: https://github.com/rajbos/GHAzDo-widget. - -> Note: only project level dashboards are supported at the moment. \ No newline at end of file diff --git a/vss-extension-dev.json b/vss-extension-dev.json index c689d35..2f0be87 100644 --- a/vss-extension-dev.json +++ b/vss-extension-dev.json @@ -1,266 +1,288 @@ { - "manifestVersion": 1, - "id": "GHAzDoWidget-DEV", - "version": "0.2.209", - "public": false, - "name": "Advanced Security dashboard Widgets [DEV]", - "description": "[DEV] GitHub Advanced Security for Azure DevOps dashboard widgets", - "publisher": "RobBos", - "categories": [ - "Azure Boards", - "Azure Repos", - "Azure Pipelines" - ], - "scopes": [ - "vso.profile", - "vso.code", - "vso.advsec" + "manifestVersion": 1, + "id": "GHAzDoWidget-DEV", + "version": "0.2.278", + "public": false, + "name": "Advanced Security dashboard Widgets [DEV]", + "description": "[DEV] GitHub Advanced Security for Azure DevOps dashboard widgets", + "publisher": "RobBos", + "categories": [ + "Azure Boards", + "Azure Repos", + "Azure Pipelines" + ], + "scopes": [ + "vso.profile", + "vso.code", + "vso.advsec" + ], + "targets": [ + { + "id": "Microsoft.VisualStudio.Services.Cloud" + } + ], + "tags": [ + "Dashboards", + "GitHub Advanced Security", + "GHAS", + "GHAzDo", + "widget", + "Security alerts", + "Pull requests" + ], + "icons": { + "default": "img/logo.png" + }, + "content": { + "details": { + "path": "overview.md" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/rajbos/GHAzDo-widget" + }, + "bugs": { + "url": "https://github.com/rajbos/GHAzDo-widget/issues" + }, + "contributions": [ + { + "id": "GHAzDoWidget.PR-tab", + "type": "ms.vss-web.tab", + "targets": [ + "ms.vss-code-web.pr-tabs" ], - "targets": [ - { - "id": "Microsoft.VisualStudio.Services.Cloud" - } - ], - "tags": [ - "Dashboards", - "GitHub Advanced Security", - "GHAS", - "GHAzDo", - "widget", - "Security alerts", - "Pull requests" - ], - "icons": { - "default": "img/logo.png" + "properties": { + "name": "GHAzDo Alerts", + "title": "GHAzDo Alerts - Pull Requests", + "uri": "widgets/pr-tab/index.html", + "action": "Advanced Security Alerts", + "dynamic": true + } }, - "content": { - "details": { - "path": "overview.md" - } + { + "id": "GHAzDoWidget.build-info-tab", + "type": "ms.vss-build-web.build-results-tab", + "description": "Advanced Security Alerts", + "targets": [ + "ms.vss-build-web.build-results-view" + ], + "properties": { + "name": "Advanced Security Alerts", + "uri": "widgets/build-info/index.html", + "title": "GHAzDo Alerts - Build Info (ms.vss-build-web.build-results-view)", + "dynamic": true + } + }, + { + "id": "GHAzDoWidget", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Configuration" + ], + "properties": { + "name": "[DEV] GHAzDO alert information", + "description": "[DEV] Display the amount of active security alerts from GitHub Advanced Security", + "catalogIconUrl": "img/publogo.png", + "previewImageUrl": "img/preview.png", + "uri": "widgets/widgets/widget_2x1/widget_2x1.html", + "supportedSizes": [ + { + "rowSpan": 1, + "columnSpan": 2 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Configuration", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Configuration", + "description": "Configures GHAzDoWidget", + "uri": "widgets/widgets/widget_2x1/configuration_2x1.html" + } + }, + { + "id": "GHAzDoWidget.1x1", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Configuration_1x1" + ], + "properties": { + "name": "[DEV] GHAzDO single alert type information", + "description": "[DEV] Display the amount of active security alerts from GitHub Advanced Security for a single type of alert", + "catalogIconUrl": "img/publogo.png", + "previewImageUrl": "img/preview.png", + "uri": "widgets/widgets/widget_1x1/widget_1x1.html", + "supportedSizes": [ + { + "rowSpan": 1, + "columnSpan": 1 + }, + { + "rowSpan": 1, + "columnSpan": 2 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Configuration_1x1", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Configuration", + "description": "Configures GHAzDoWidget", + "uri": "widgets/widgets/widget_1x1/configuration_1x1.html" + } + }, + { + "id": "GHAzDoWidget.Chart", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Chart.Configuration" + ], + "properties": { + "name": "[DEV] GHAzDoWidget - Chart", + "description": "[DEV] A trend chart widget for Advanced Security alerts.", + "catalogIconUrl": "img/publogo.png", + "uri": "widgets/widgets/chart/chart.html", + "supportedSizes": [ + { + "rowSpan": 2, + "columnSpan": 2 + }, + { + "rowSpan": 2, + "columnSpan": 3 + }, + { + "rowSpan": 2, + "columnSpan": 4 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Chart.Configuration", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Chart Configuration", + "description": "Configures GHAzDoWidget.Chart", + "uri": "widgets/widgets/chart/configuration_2x2.html" + } + }, + { + "id": "GHAzDoWidget.TestingWidget", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog" + ], + "properties": { + "name": "[DEV] GHAzDO testing widget", + "description": "[DEV] test stuff", + "catalogIconUrl": "img/publogo.png", + "previewImageUrl": "img/preview.png", + "uri": "widgets/widgets/testing_widget/testing.html", + "supportedSizes": [ + { + "rowSpan": 4, + "columnSpan": 3 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Hub", + "type": "ms.vss-web.hub", + "description": "[DEV] GHAzDO Hub", + "targets": [ + "ms.vss-work-web.work-hub-group", + "ms.vss-code-web.code-hub-group" + ], + "properties": { + "name": "Advanced Security Dashboard", + "uri": "widgets/widgets/hub/hub.html", + "iconName": "Shield", + "order": 99 + } + }, + { + "id": "GHAzDoDependencyReviewTask", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "dependencyReviewTask" + } + } + ], + "files": [ + { + "path": "widgets/widgets", + "addressable": true + }, + { + "path": "widgets/build-info", + "addressable": true + }, + { + "path": "widgets/pr-tab", + "addressable": true + }, + { + "path": "widgets/library.js", + "addressable": true + }, + { + "path": "widgets/styles.css", + "addressable": true + }, + { + "path": "img", + "addressable": true + }, + { + "path": "dependencyReviewTask/index.js", + "addressable": true }, - "repository": { - "type": "git", - "url": "https://github.com/rajbos/GHAzDo-widget" + { + "path": "dependencyReviewTask/task.json", + "addressable": true }, - "bugs": { - "url": "https://github.com/rajbos/GHAzDo-widget/issues" + { + "path": "dependencyReviewTask/node_modules/", + "addressable": true }, - "contributions": [ - { - "id": "GHAzDoWidget.PR-tab", - "type": "ms.vss-web.tab", - "targets": [ - "ms.vss-code-web.pr-tabs" - ], - "properties": { - "name": "GHAzDo Alerts", - "title": "GHAzDo Alerts - Pull Requests", - "uri": "widgets/pr-tab/index.html", - "action": "Advanced Security Alerts", - "dynamic": true - } - }, - { - "id": "GHAzDoWidget.build-info-tab", - "type": "ms.vss-build-web.build-results-tab", - "description": "Advanced Security Alerts", - "targets": [ - "ms.vss-build-web.build-results-view" - ], - "properties": { - "name": "Advanced Security Alerts", - "uri": "widgets/build-info/index.html", - "title":"GHAzDo Alerts - Build Info (ms.vss-build-web.build-results-view)", - "dynamic": true - } - }, - { - "id": "GHAzDoWidget", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Configuration" - ], - "properties": { - "name": "[DEV] GHAzDO alert information", - "description": "[DEV] Display the amount of active security alerts from GitHub Advanced Security", - "catalogIconUrl": "img/publogo.png", - "previewImageUrl": "img/preview.png", - "uri": "widgets/widgets/widget_2x1/widget_2x1.html", - "supportedSizes": [ - { - "rowSpan": 1, - "columnSpan": 2 - } - ], - "supportedScopes": ["project_team"] - } - }, - { - "id": "GHAzDoWidget.Configuration", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Configuration", - "description": "Configures GHAzDoWidget", - "uri": "widgets/widgets/widget_2x1/configuration_2x1.html" - } - }, - { - "id": "GHAzDoWidget.1x1", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Configuration_1x1" - ], - "properties": { - "name": "[DEV] GHAzDO single alert type information", - "description": "[DEV] Display the amount of active security alerts from GitHub Advanced Security for a single type of alert", - "catalogIconUrl": "img/publogo.png", - "previewImageUrl": "img/preview.png", - "uri": "widgets/widgets/widget_1x1/widget_1x1.html", - "supportedSizes": [ - { - "rowSpan": 1, - "columnSpan": 1 - }, - { - "rowSpan": 1, - "columnSpan": 2 - }], - "supportedScopes": ["project_team"] - } - }, - { - "id": "GHAzDoWidget.Configuration_1x1", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Configuration", - "description": "Configures GHAzDoWidget", - "uri": "widgets/widgets/widget_1x1/configuration_1x1.html" - } - }, - { - "id": "GHAzDoWidget.Chart", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget-DEV.GHAzDoWidget.Chart.Configuration" - ], - "properties": { - "name": "[DEV] GHAzDoWidget - Chart", - "description": "[DEV] A trend chart widget for Advanced Security alerts.", - "catalogIconUrl": "img/publogo.png", - "uri": "widgets/widgets/chart/chart.html", - "supportedSizes": [ - { - "rowSpan": 2, - "columnSpan": 2 - }, - { - "rowSpan": 2, - "columnSpan": 3 - }, - { - "rowSpan": 2, - "columnSpan": 4 - } - ], - "supportedScopes": [ - "project_team" - ] - } - }, - { - "id": "GHAzDoWidget.Chart.Configuration", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Chart Configuration", - "description": "Configures GHAzDoWidget.Chart", - "uri": "widgets/widgets/chart/configuration_2x2.html" - } - }, - { - "id": "GHAzDoWidget.TestingWidget", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog" - ], - "properties": { - "name": "[DEV] GHAzDO testing widget", - "description": "[DEV] test stuff", - "catalogIconUrl": "img/publogo.png", - "previewImageUrl": "img/preview.png", - "uri": "widgets/widgets/testing_widget/testing.html", - "supportedSizes": [ - { - "rowSpan": 4, - "columnSpan": 3 - } - ], - "supportedScopes": ["project_team"] - } - }, - { - "id": "GHAzDoWidget.Hub", - "type": "ms.vss-web.hub", - "description": "[DEV] GHAzDO Hub", - "targets": [ - "ms.vss-work-web.work-hub-group", - "ms.vss-code-web.code-hub-group" - ], - "properties": { - "name": "Advanced Security Dashboard", - "uri": "widgets/widgets/hub/hub.html", - "iconName": "Shield", - "order": 99 - } - }, - { - "id": "GHAzDoDependencyReviewTask", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "dependencyReviewTask" - } - } - ], - "files": [ - { - "path": "widgets/widgets", "addressable": true - }, - { - "path": "widgets/build-info", "addressable": true - }, - { - "path": "widgets/pr-tab", "addressable": true - }, - { - "path": "widgets/library.js", "addressable": true - }, - { - "path": "widgets/styles.css", "addressable": true - }, - { - "path": "img", "addressable": true - }, - { - "path": "dependencyReviewTask/index.js", "addressable": true - }, - { - "path": "dependencyReviewTask/task.json", "addressable": true - }, - { - "path": "dependencyReviewTask/node_modules/", "addressable": true - }, - { - "path": "widgets/node_modules/vss-web-extension-sdk/lib", - "addressable": true, - "packagePath": "lib" - } - ] -} \ No newline at end of file + { + "path": "widgets/node_modules/vss-web-extension-sdk/lib", + "addressable": true, + "packagePath": "lib" + } + ] +} diff --git a/vss-extension.json b/vss-extension.json index 24a00f7..e95ca1e 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -1,173 +1,211 @@ { - "manifestVersion": 1, - "id": "GHAzDoWidget", - "version": "0.0.1.8", - "public": true, - "name": "Advanced Security dashboard Widgets", - "description": "GitHub Advanced Security for Azure DevOps dashboard widgets", - "publisher": "RobBos", - "categories": ["Azure Boards"], - "scopes": [ - "vso.profile", - "vso.code", - "vso.advsec" + "manifestVersion": 1, + "id": "GHAzDoWidget", + "version": "0.0.1.11", + "public": true, + "name": "Advanced Security dashboard Widgets", + "description": "GitHub Advanced Security for Azure DevOps dashboard widgets", + "publisher": "RobBos", + "categories": [ + "Azure Boards" + ], + "scopes": [ + "vso.profile", + "vso.code", + "vso.advsec" + ], + "targets": [ + { + "id": "Microsoft.VisualStudio.Services.Cloud" + } + ], + "tags": [ + "Dashboards", + "GitHub Advanced Security", + "GHAS", + "GHAzDo", + "widget", + "Security alerts" + ], + "icons": { + "default": "img/logo.png" + }, + "content": { + "details": { + "path": "overview.md" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/rajbos/GHAzDo-widget" + }, + "bugs": { + "url": "https://github.com/rajbos/GHAzDo-widget/issues" + }, + "contributions": [ + { + "id": "GHAzDoWidget", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget.GHAzDoWidget.Configuration" ], - "targets": [ - { - "id": "Microsoft.VisualStudio.Services.Cloud" - } - ], - "tags": [ - "Dashboards", - "GitHub Advanced Security", - "GHAS", - "GHAzDo", - "widget", - "Security alerts" - ], - "icons": { - "default": "img/logo.png" + "properties": { + "name": "GHAzDO alert information", + "description": "Display the amount of active security alerts from GitHub Advanced Security", + "catalogIconUrl": "img/publogo.png", + "previewImageUrl": "img/preview.png", + "uri": "widgets/widgets/widget_2x1/widget_2x1.html", + "supportedSizes": [ + { + "rowSpan": 1, + "columnSpan": 2 + } + ], + "supportedScopes": [ + "project_team" + ] + } }, - "content": { - "details": { - "path": "overview.md" - } + { + "id": "GHAzDoWidget.Configuration", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Configuration", + "description": "Configures GHAzDoWidget", + "uri": "widgets/widgets/widget_2x1/configuration_2x1.html" + } + }, + { + "id": "GHAzDoWidget.1x1", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget.GHAzDoWidget.Configuration_1x1" + ], + "properties": { + "name": "GHAzDO single alert type information", + "description": "Display the amount of active security alerts from GitHub Advanced Security for a single type of alert", + "catalogIconUrl": "img/publogo.png", + "previewImageUrl": "img/preview.png", + "uri": "widgets/widgets/widget_1x1/widget_1x1.html", + "supportedSizes": [ + { + "rowSpan": 1, + "columnSpan": 1 + }, + { + "rowSpan": 1, + "columnSpan": 2 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Configuration_1x1", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Configuration", + "description": "Configures GHAzDoWidget", + "uri": "widgets/widgets/widget_1x1/configuration_1x1.html" + } + }, + { + "id": "GHAzDoWidget.Chart", + "type": "ms.vss-dashboards-web.widget", + "targets": [ + "ms.vss-dashboards-web.widget-catalog", + "RobBos.GHAzDoWidget.GHAzDoWidget.Chart.Configuration" + ], + "properties": { + "name": "GHAzDoWidget - Chart", + "description": "A trend chart widget for Advanced Security alerts.", + "catalogIconUrl": "img/publogo.png", + "uri": "widgets/widgets/chart/chart.html", + "supportedSizes": [ + { + "rowSpan": 2, + "columnSpan": 2 + }, + { + "rowSpan": 2, + "columnSpan": 3 + }, + { + "rowSpan": 2, + "columnSpan": 4 + } + ], + "supportedScopes": [ + "project_team" + ] + } + }, + { + "id": "GHAzDoWidget.Chart.Configuration", + "type": "ms.vss-dashboards-web.widget-configuration", + "targets": [ + "ms.vss-dashboards-web.widget-configuration" + ], + "properties": { + "name": "GHAzDoWidget Chart Configuration", + "description": "Configures GHAzDoWidget.Chart", + "uri": "widgets/widgets/chart/configuration_2x2.html" + } + }, + { + "id": "GHAzDoDependencyReviewTask", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "dependencyReviewTask" + } + } + ], + "files": [ + { + "path": "widgets/widgets", + "addressable": true + }, + { + "path": "widgets/library.js", + "addressable": true + }, + { + "path": "widgets/styles.css", + "addressable": true + }, + { + "path": "img", + "addressable": true + }, + { + "path": "dependencyReviewTask/index.js", + "addressable": true }, - "repository": { - "type": "git", - "url": "https://github.com/rajbos/GHAzDo-widget" + { + "path": "dependencyReviewTask/task.json", + "addressable": true }, - "bugs": { - "url": "https://github.com/rajbos/GHAzDo-widget/issues" + { + "path": "dependencyReviewTask/node_modules/", + "addressable": true }, - "contributions": [ - { - "id": "GHAzDoWidget", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget.GHAzDoWidget.Configuration" - ], - "properties": { - "name": "GHAzDO alert information", - "description": "Display the amount of active security alerts from GitHub Advanced Security", - "catalogIconUrl": "img/publogo.png", - "previewImageUrl": "img/preview.png", - "uri": "widgets/widget_2x1/widget_2x1.html", - "supportedSizes": [ - { - "rowSpan": 1, - "columnSpan": 2 - } - ], - "supportedScopes": ["project_team"] - } - }, - { - "id": "GHAzDoWidget.Configuration", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Configuration", - "description": "Configures GHAzDoWidget", - "uri": "widgets/widget_2x1/configuration_2x1.html" - } - }, - { - "id": "GHAzDoWidget.1x1", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget.GHAzDoWidget.Configuration_1x1" - ], - "properties": { - "name": "GHAzDO single alert type information", - "description": "Display the amount of active security alerts from GitHub Advanced Security for a single type of alert", - "catalogIconUrl": "img/publogo.png", - "previewImageUrl": "img/preview.png", - "uri": "widgets/widget_1x1/widget_1x1.html", - "supportedSizes": [ - { - "rowSpan": 1, - "columnSpan": 1 - }, - { - "rowSpan": 1, - "columnSpan": 2 - } - ], - "supportedScopes": ["project_team"] - } - }, - { - "id": "GHAzDoWidget.Configuration_1x1", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Configuration", - "description": "Configures GHAzDoWidget", - "uri": "widgets/widget_1x1/configuration_1x1.html" - } - }, - { - "id": "GHAzDoWidget.Chart", - "type": "ms.vss-dashboards-web.widget", - "targets": [ - "ms.vss-dashboards-web.widget-catalog", - "RobBos.GHAzDoWidget.GHAzDoWidget.Chart.Configuration" - ], - "properties": { - "name": "GHAzDoWidget - Chart", - "description": "A trend chart widget for Advanced Security alerts.", - "catalogIconUrl": "img/publogo.png", - "uri": "widgets/chart/chart.html", - "supportedSizes": [ - { - "rowSpan": 2, - "columnSpan": 2 - }, - { - "rowSpan": 2, - "columnSpan": 3 - }, - { - "rowSpan": 2, - "columnSpan": 4 - } - ], - "supportedScopes": [ - "project_team" - ] - } - }, - { - "id": "GHAzDoWidget.Chart.Configuration", - "type": "ms.vss-dashboards-web.widget-configuration", - "targets": [ "ms.vss-dashboards-web.widget-configuration" ], - "properties": { - "name": "GHAzDoWidget Chart Configuration", - "description": "Configures GHAzDoWidget.Chart", - "uri": "widgets/chart/configuration_2x2.html" - } - } - ], - "files": [ - { - "path": "widgets", "addressable": true - }, - { - "path": "library.js", "addressable": true - }, - { - "path": "styles.css", "addressable": true - }, - { - "path": "img", "addressable": true - }, - { - "path": "node_modules/vss-web-extension-sdk/lib", - "addressable": true, - "packagePath": "lib" - } - ] -} \ No newline at end of file + { + "path": "widgets/node_modules/vss-web-extension-sdk/lib", + "addressable": true, + "packagePath": "lib" + } + ] +} diff --git a/widgets/library.js b/widgets/library.js index 8e2e998..245325c 100644 --- a/widgets/library.js +++ b/widgets/library.js @@ -1,3 +1,7 @@ +// global variables +const areaName = "alert" // old: 'AdvancedSecurity', new: 'alert' todo: rename to alerts when CORS issues are fixed +const apiVersion = "7.2-preview.1" + function getAuthHeader() { return new Promise((resolve, reject) => { VSS.require(["VSS/Authentication/Services"], function( @@ -69,9 +73,36 @@ function GetAlertTypeFromValue(value) { } } -async function getAlerts(organization, projectName, repoId) { - //consoleLog('getAlerts'); +function fillSelectRepoDropdown(dropDown, repos) { + // add a top option to select no repo + dropDown.append(``); + dropDown.append(``); + // sort the repo alphabetically + repos.sort((a, b) => a.name.localeCompare(b.name)); + repos.forEach(r => { + dropDown.append(``); + }); +} +async function getAlerts(organization, projectName, repoId, repos) { + if (repoId) { + // run normally for a single repo + return await getAlertsForRepo(organization, projectName, repoId) + } + else { + // todo: run for ALL repositories in the current project + // load all repos in the project + return { + count: -1, + dependencyAlerts: -1, + secretAlerts: -1, + codeAlerts: -1 + } + } +} + +async function getAlertsForRepo(organization, projectName, repoId) { + //consoleLog('getAlerts'); let values = { count: 0, dependencyAlerts: 0, @@ -81,7 +112,7 @@ async function getAlerts(organization, projectName, repoId) { try { // first check if GHAzDo is enabled or not - url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/Management/repositories/${repoId}/enablement?api-version=7.2-preview.1` + url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/management/repositories/${repoId}/enablement?api-version=${apiVersion}`; //const featuresEnabledResult = await authenticatedGet(url); //authenticatedGet(url).then(featuresEnabledResult => { @@ -91,7 +122,8 @@ async function getAlerts(organization, projectName, repoId) { // } // todo: use pagination option, now: get the first 5000 alerts - url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/AdvancedSecurity/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=true&criteria.states=1&api-version=7.2-preview.1`; + console.log(`Getting alerts for repo [${repoId}]`); + url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/${areaName}/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=true&criteria.states=1&api-version=${apiVersion}`; //consoleLog(`Calling url: [${url}]`); const alertResult = await authenticatedGet(url); //authenticatedGet(url).then(alertResult => { @@ -128,7 +160,7 @@ 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=true&api-version=7.2-preview.1`; + url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/${areaName}/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=true&api-version=${apiVersion}`; consoleLog(`Calling url: [${url}]`); const alertResult = await authenticatedGet(url); //consoleLog('alertResult: ' + JSON.stringify(alertResult)); @@ -330,6 +362,7 @@ async function getProjects(VSS, Service, CoreRestClient) { async function getRepos(VSS, Service, GitWebApi, projectName, useCache = true) { + consoleLog(`inside getRepos`); const webContext = VSS.getWebContext(); const project = webContext.project; let projectNameForSearch = projectName ? projectName : project.name; @@ -345,7 +378,7 @@ async function getRepos(VSS, Service, GitWebApi, projectName, useCache = true) { try { const document = await getSavedDocument(VSS, documentCollection, documentId); consoleLog(`document inside getRepos: ${JSON.stringify(document)}`); - if (document || document.data.length > 0) { + if (document || document?.data?.length > 0) { consoleLog(`Loaded repos from document store. Last updated [${document.lastUpdated}]`); // get the data type of lastUpdated consoleLog(`typeof document.lastUpdated: ${typeof document.lastUpdated}`) @@ -367,7 +400,7 @@ async function getRepos(VSS, Service, GitWebApi, projectName, useCache = true) { } } - consoleLog(`Loading repositories from the API`); + consoleLog(`Loading repositories from the API for project [${projectNameForSearch}]`); try { const gitClient = Service.getClient(GitWebApi.GitHttpClient); let repos = await gitClient.getRepositories(projectNameForSearch); @@ -408,7 +441,7 @@ async function getAlertSeverityCounts(organization, projectName, repoId, alertTy ]; try { // todo: filter on alertType - url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/AdvancedSecurity/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=true&criteria.alertType=${alertType.value}&criteria.states=1&api-version=7.2-preview.1`; + url = `https://advsec.dev.azure.com/${organization}/${projectName}/_apis/${areaName}/repositories/${repoId}/alerts?top=5000&criteria.onlyDefaultBranchAlerts=true&criteria.alertType=${alertType.value}&criteria.states=1&api-version=${apiVersion}`; //consoleLog(`Calling url: [${url}]`); const alertResult = await authenticatedGet(url); //consoleLog('alertResult: ' + JSON.stringify(alertResult)); diff --git a/widgets/widgets/widget_1x1/configuration_1x1.html b/widgets/widgets/widget_1x1/configuration_1x1.html index d401daa..bfa4955 100644 --- a/widgets/widgets/widget_1x1/configuration_1x1.html +++ b/widgets/widgets/widget_1x1/configuration_1x1.html @@ -31,7 +31,7 @@ var customSettings = { data: JSON.stringify({ repo: $repoDropdown.val(), - repoId: repo.id, + repoId: repo?.id, repoAlertType: $repoAlertType.val() }) }; @@ -46,13 +46,8 @@ // add all repos as selection options to the dropdown if (repos) { - // add a top option to select no repo - $repoDropdown.append(``); - // sort the repo alphabetically - repos.sort((a, b) => a.name.localeCompare(b.name)); - repos.forEach(r => { - $repoDropdown.append(``); - }); + // todo: use in all other widget locations as well + fillSelectRepoDropdown($repoDropdown, repos) } if (settings && settings.repo) { @@ -85,7 +80,7 @@ var customSettings = { data: JSON.stringify({ repo: $repoDropdown.val(), - repoId: repo.id, + repoId: repo?.id, repoAlertType: $repoAlertType.val() }) }; diff --git a/widgets/widgets/widget_1x1/widget_1x1.html b/widgets/widgets/widget_1x1/widget_1x1.html index ba1f6da..204e92c 100644 --- a/widgets/widgets/widget_1x1/widget_1x1.html +++ b/widgets/widgets/widget_1x1/widget_1x1.html @@ -13,11 +13,11 @@ }); VSS.require( - ["TFS/Dashboards/WidgetHelpers", "VSS/Context", "VSS/Authentication/Services"], - async function (WidgetHelpers, context) + ["VSS/Service", "TFS/Dashboards/WidgetHelpers", "VSS/Context", "TFS/VersionControl/GitRestClient", "TFS/Core/RestClient"], + async function (Service, WidgetHelpers, context, GitWebApi, RestClient) { WidgetHelpers.IncludeWidgetStyles(); - VSS.register("GHAzDoWidget.1x1", function () { + VSS.register("GHAzDoWidget.1x1", async function () { const webContext = VSS.getWebContext(); const project = webContext.project; const organization = webContext.account.name; @@ -29,15 +29,22 @@ consoleLog('project name: ' + projectName); consoleLog('organization name: ' + organization); + // log if vss, service, gitwebapi have a value or not + // consoleLog(`vss: [${VSS}], GitWebApi: [${JSON.stringify(GitWebApi)}]`) + // consoleLog(`GitWebApi.GitHttpClient: [${GitWebApi.GitHttpClient}]`); + + // const repos = await getRepos(VSS, Service, GitWebApi, projectName, useCache = false); + // consoleLog(`Found repos:[${repos.length}]`); + return { load: async function (widgetSettings) { - await loadWidget(widgetSettings, organization, projectName); + await loadWidget(widgetSettings, organization, projectName, VSS, Service, GitWebApi); return WidgetHelpers.WidgetStatusHelper.Success(); }, reload: async function (widgetSettings) { consoleLog('reload with widgetSettings: ' + JSON.stringify(widgetSettings)); - await loadWidget(widgetSettings, organization, projectName); + await loadWidget(widgetSettings, organization, projectName, VSS, Service, GitWebApi); return; } } diff --git a/widgets/widgets/widget_1x1/widget_1x1.js b/widgets/widgets/widget_1x1/widget_1x1.js index e0d3eba..abe92ec 100644 --- a/widgets/widgets/widget_1x1/widget_1x1.js +++ b/widgets/widgets/widget_1x1/widget_1x1.js @@ -1,9 +1,10 @@ -async function loadWidget(widgetSettings, organization, projectName) { +async function loadWidget(widgetSettings, organization, projectName, VSS, Service, GitWebApi) { consoleLog(`WidgetSettings inside loadWidget_1x1: ${JSON.stringify(widgetSettings)}`); consoleLog(`Running for organization [${organization}], projectName [${projectName}]`); // data contains a stringified json object, so we need to make a json object from it const data = JSON.parse(widgetSettings.customSettings.data); + consoleLog(`data from the widgetSettings: ${JSON.stringify(data)}`); // init with default values let alerts = { @@ -13,19 +14,43 @@ async function loadWidget(widgetSettings, organization, projectName) { }; let linkBase = 'https://dev.azure.com'; - if (data && data.repo) { - const repoName = data.repo; + if (data) { const repoId = data.repoId; - consoleLog(`loaded repoName from widgetSettings_1x1: [${repoName}] and id [${repoId}]`); + let repoName = ""; + if (data.repo) { + repoName = data.repo + consoleLog(`loaded repoName from widgetSettings_1x1: [${repoName}] and id [${repoId}]`); - // set the tile + alerts = await getAlerts(organization, projectName, repoId); + } + else { + // load alerts for ALL repos in the project + repoName = `${projectName}`; + // todo: load all + consoleLog(`loading alerts for all repos in the project [${repoName}]`); + + const repos = await getRepos(VSS, Service, GitWebApi, projectName, useCache = true) + for (let repoIndex in repos) { + const repo = repos[repoIndex]; + consoleLog(`loading alerts for repo [${repo.name}] with id [${repo.id}]`); + // call and let the promise handle the rest + const repoAlerts = await getAlerts(organization, projectName, repo.id) + + alerts.codeAlerts += repoAlerts.codeAlerts; + alerts.dependencyAlerts += repoAlerts.dependencyAlerts; + alerts.secretAlerts += repoAlerts.secretAlerts; + } + } + consoleLog('alerts: ' + JSON.stringify(alerts)); + + // remove %20 from the name so that it displays correctly + repoName = repoName.replace(/%20/g, ' '); //todo: support more of these weird characters + + // set the title var title = $('h2.ghazdo-title'); title.text(`${repoName}`); title.attr('title', repoName); - consoleLog(`title set to [${repoName}]`); - alerts = await getAlerts(organization, projectName, repoId); - consoleLog('alerts: ' + JSON.stringify(alerts)); // GHAS is only available on the SaaS version, so we can hardcode the domain linkBase = `https://dev.azure.com/${organization}/${projectName}/_git/${repoName}/alerts`; @@ -39,8 +64,12 @@ async function loadWidget(widgetSettings, organization, projectName) { let alertTypeToShow = 1 if (data && data.repoAlertType) { + consoleLog(`loaded repoAlertType from widgetSettings_1x1: [${data.repoAlertType}]`); alertTypeToShow = data.repoAlertType; } + else { + consoleLog(`repoAlertType not found in widgetSettings_1x1, defaulting to [${alertTypeToShow}]`); + } // set the alert count consoleLog(`Setting the alert count 1x1 for type ${alertTypeToShow}`); diff --git a/widgets/widgets/widget_2x1/widget_2x1.html b/widgets/widgets/widget_2x1/widget_2x1.html index 6c38fc1..92340c3 100644 --- a/widgets/widgets/widget_2x1/widget_2x1.html +++ b/widgets/widgets/widget_2x1/widget_2x1.html @@ -13,7 +13,7 @@ }); VSS.require( - ["TFS/Dashboards/WidgetHelpers", "VSS/Context", "VSS/Authentication/Services"], + ["TFS/Dashboards/WidgetHelpers", "VSS/Context"], async function (WidgetHelpers, context) { WidgetHelpers.IncludeWidgetStyles(); diff --git a/z-status.txt b/z-status.txt new file mode 100644 index 0000000..5a4c8a8 --- /dev/null +++ b/z-status.txt @@ -0,0 +1,7 @@ +In this branch was added: +- in the 1x1 widget, code was added to configure this widget for either 1 repo or ALL repos in the project + - test loading the config, both in the config screen and the widget itself + - write the loop to go over ALL repos in the org (might be already be available in another branch) + + +Do check the versions of the widget before pushing! \ No newline at end of file