Skip to content

Commit 570d85f

Browse files
committed
Initial commit for Azure DevOps - DevSecOps - Advanced Security - E2E Demo
0 parents  commit 570d85f

File tree

478 files changed

+22973
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

478 files changed

+22973
-0
lines changed

.azuredevops/appsec/CIGate.ps1

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# This script is used as part of our PR gating strategy. It takes advantage of the GHAzDO REST API to check for Code Scanning and Dependency Scanning issues a PR source and target branch.
2+
# If there are 'new' issues in the source branch, the script will fail with error code 1.
3+
# The script will also log errors, 1 per new Code Scanning/Dependency alert, it will also add PR annotations for the alert
4+
$pat = ${env:MAPPED_ADO_PAT}
5+
$orgUri = ${env:SYSTEM_COLLECTIONURI}
6+
$orgName = $orgUri -replace "^https://dev.azure.com/|/$"
7+
$project = ${env:SYSTEM_TEAMPROJECT}
8+
$repositoryId = ${env:BUILD_REPOSITORY_ID}
9+
$prTargetBranch = ${env:SYSTEM_PULLREQUEST_TARGETBRANCH}
10+
$prSourceBranch = ${env:BUILD_SOURCEBRANCH}
11+
$prId = ${env:SYSTEM_PULLREQUEST_PULLREQUESTID}
12+
$prCurrentIteration = ${env:SYSTEM_PULLREQUEST_PULLREQUESTITERATION}
13+
$buildReason = ${env:BUILD_REASON}
14+
$sourceDir = ${env:BUILD_SOURCESDIRECTORY}
15+
$headers = @{ Authorization = "Basic $([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(($pat.Contains(":") ? $pat : ":$pat"))))" }
16+
#Get-ChildItem Env: | Format-Table -AutoSize
17+
18+
#GATING POLICY - Which alerts to check for and which severities to include
19+
$alertTypes = @("dependency", "code")
20+
$severityPolicy = @{
21+
"dependency" = @("critical", "high") #"medium", "low"
22+
"code" = @("critical", "high", "error") #"medium", "low", "warning", "note" #Security and Quality Severities
23+
}
24+
25+
# Alerts - List api: https://learn.microsoft.com/en-us/rest/api/azure/devops/advancedsecurity/alerts/list (criteria.states = 1 means open alerts, criteria.alertType does not support multiple values, so we need to allow default and filter out later)
26+
$urlTargetAlerts = "https://advsec.dev.azure.com/{0}/{1}/_apis/Alert/repositories/{2}/Alerts?top=500&orderBy=lastSeen&criteria.ref={3}&criteria.states=1" -f $orgName, $project, $repositoryId, $prTargetBranch
27+
$urlSourceAlerts = "https://advsec.dev.azure.com/{0}/{1}/_apis/Alert/repositories/{2}/Alerts?top=500&orderBy=lastSeen&criteria.ref={3}&criteria.states=1" -f $orgName, $project, $repositoryId, $prSourceBranch
28+
29+
#PR Threads - Create api: https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/create
30+
$urlComment = "https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/pullRequests/{3}/threads?api-version=7.1-preview.1" -f $orgName, $project, $repositoryId, $prId
31+
#PR Iteration Changes - Get API: https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-iteration-changes/get
32+
$urlIteration = "https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/pullRequests/{3}/iterations/{4}/changes?api-version=7.1-preview.1&`$compareTo={5}" -f $orgName, $project, $repositoryId, $prId, $prCurrentIteration, ($prCurrentIteration - 1)
33+
34+
# Add a PR annotations for the Alert in the changed file. This is only intended for alerts where the alert intersects with source code found in the PR diff.
35+
function AddPRComment($prAlert, $urlAlert) {
36+
$pathToCheck = $prAlert.physicalLocations[-1].filePath
37+
38+
## Todo - potentially improve this for transitive dependencies by walking the path to parent(will need to dedup as Dependency scanning will report findings on transitive manifests such as /node_modules/x/package.json )
39+
if ($prAlert.alertType -eq "dependency") {
40+
#dependency alerts physicalLocations always begin with the last directory in sourceDir (ex: 's/package.json'), so parse it out
41+
#$sourceDirSegment = Split-Path $sourceDir -Leaf ###also works but harder to test locally as it needs a real Path :)
42+
$sourceDirSegment = $sourceDir.Split([System.IO.Path]::DirectorySeparatorChar)[-1]
43+
$pathToCheck = $pathToCheck.TrimStart($sourceDirSegment)
44+
}
45+
elseif($prAlert.alertType -eq "code") {
46+
$pathToCheck = "/" + $pathToCheck
47+
}
48+
49+
# Get Pull Request iterations, we need this to map the file to a changeTrackingId
50+
$prIterations = Invoke-RestMethod -Uri $urlIteration -Method Get -Headers $headers
51+
52+
# Find the changeTrackingId mapping to the file with the Code Scanning alert
53+
$iterationItem = $prIterations.changeEntries | Where-Object { $_.item.path -like $pathToCheck } | Select-Object -First 1
54+
55+
# Any change to the file with the alert in this PR iteration?
56+
if ($null -eq $iterationItem) {
57+
Write-Host "##[debug] In this iteration of the PR:Iteration $prCurrentIteration, there is no change to the file where the alert was detected: $pathToCheck."
58+
return
59+
}
60+
61+
if ($prAlert.alertType -eq "dependency") {
62+
# Define the Body hashtable
63+
# Dependency alerts do not have line numbers, so we will not add a line number to the comment
64+
$body = @{
65+
"comments" = @(
66+
@{
67+
"content" = "**$($prAlert.title)**
68+
$($prAlert.tools.rules.description)
69+
See details [here]($($urlAlert))"
70+
"commentType" = 1
71+
}
72+
)
73+
"status" = 1
74+
"threadContext" = @{
75+
"filePath" = "./$($prAlert.physicalLocations[-1].filePath)"
76+
}
77+
"pullRequestThreadContext" = @{
78+
"changeTrackingId" = $($iterationItem.changeTrackingId)
79+
"iterationContext" = @{
80+
"firstComparingIteration" = $($prCurrentIteration)
81+
"secondComparingIteration" = $($prCurrentIteration)
82+
}
83+
}
84+
}
85+
}
86+
elseif($prAlert.alertType -eq "code") {
87+
$lineEnd = $($prAlert.physicalLocations[-1].region.lineEnd)
88+
$lineStart = $($prAlert.physicalLocations[-1].region.lineStart)
89+
90+
if ($lineStart -eq 0) {
91+
$lineStart = 1
92+
}
93+
94+
if ($lineEnd -eq 0) {
95+
$lineEnd = $lineStart
96+
}
97+
98+
# Define the Body hashtable
99+
$body = @{
100+
"comments" = @(
101+
@{
102+
"content" = "**$($prAlert.title)**
103+
$($prAlert.tools.rules.description)
104+
See details [here]($($urlAlert))"
105+
"commentType" = 1
106+
}
107+
)
108+
"status" = 1
109+
"threadContext" = @{
110+
"filePath" = "./$($prAlert.physicalLocations[-1].filePath)"
111+
"rightFileStart" = @{
112+
"line" = $lineStart
113+
"offset" = $($prAlert.physicalLocations[-1].region.columnStart)
114+
}
115+
"rightFileEnd" = @{
116+
"line" = $lineEnd
117+
"offset" = $($prAlert.physicalLocations[-1].region.columnEnd)
118+
}
119+
}
120+
"pullRequestThreadContext" = @{
121+
"changeTrackingId" = $($iterationItem.changeTrackingId)
122+
"iterationContext" = @{
123+
"firstComparingIteration" = $($prCurrentIteration)
124+
"secondComparingIteration" = $($prCurrentIteration)
125+
}
126+
}
127+
}
128+
}
129+
130+
# Convert the hashtable to a JSON string
131+
$bodyJson = $body | ConvertTo-Json -Depth 10
132+
#Write-Output $bodyJson
133+
134+
# Send the PR Threads Create POST request
135+
$response = Invoke-RestMethod -Uri $urlComment -Method Post -Headers $headers -Body $bodyJson -ContentType "application/json"
136+
137+
#Write-Output $response
138+
Write-Host "##[debug] New thread created in PR:Iteration $prCurrentIteration : $($response._links.self.href)"
139+
140+
return
141+
}
142+
143+
Write-Host "Will check to see if there are any new Dependency or Code scanning alerts in this PR branch"
144+
Write-Host "PR source : $($prSourceBranch). PR target: $($prTargetBranch)"
145+
146+
if ($buildReason -ne 'PullRequest') {
147+
Write-Host "This build is not part of a Pull Request so all is ok"
148+
exit 0
149+
}
150+
151+
# Wait
152+
Write-Host "Wait 5 mins for all other scans to finish... Should check the Build validations for the PR to see if they are all done."
153+
Start-Sleep -Seconds 300
154+
155+
# Get the alerts on the pr target branch (all without filter) and the PR source branch (only currently open)
156+
$alertsPRSource = Invoke-WebRequest -Uri $urlSourceAlerts -Headers $headers -Method Get
157+
158+
# The Advanced Security scanning of the target branch runs in a separate pipeline. This scan might not have been completed.
159+
# Try to get the results 3 times with a 1 min wait between each try.
160+
$retries = 3
161+
while ($retries -gt 0) {
162+
try {
163+
$alertsPRTarget = Invoke-WebRequest -Uri $urlTargetAlerts -Headers $headers -Method Get -ErrorAction Stop
164+
# Success
165+
break
166+
}
167+
catch {
168+
# No GHAzDO results on the target branch, wait and retry?
169+
if ($_.ErrorDetails.Message.Split("`"") -contains "BranchNotFoundException") {
170+
$retries--
171+
if ($retries -eq 0) {
172+
# We have retried the maximum number of times, give up
173+
Write-Host "##vso[task.logissue type=error] We have retried the maximum number of times, give up."
174+
throw $_
175+
}
176+
177+
# Wait and then try again
178+
Write-Host "There are no GHAzDO results on the target branch, wait and try again."
179+
Start-Sleep -Seconds 60
180+
}
181+
else {
182+
# Something else is wrong, give up
183+
Write-Host "##vso[task.logissue type=error] There was an unexpected error."
184+
throw $_
185+
}
186+
}
187+
}
188+
189+
if ($alertsPRTarget.StatusCode -ne 200) {
190+
Write-Host "##vso[task.logissue type=error] Error getting alerts from Azure DevOps Advanced Security PR target branch:", $alertsPRTarget.StatusCode, $alertsPRTarget.StatusDescription
191+
exit 1
192+
}
193+
194+
if ($alertsPRSource.StatusCode -ne 200) {
195+
Write-Host "##vso[task.logissue type=error] Error getting alerts from Azure DevOps Advanced Security PR source branch:", $alertsPRSource.StatusCode, $alertsPRSource.StatusDescription
196+
exit 1
197+
}
198+
199+
# Filter out the alert types that we are interested in
200+
$jsonPRTarget = ($alertsPRTarget.Content | ConvertFrom-Json).value | Where-Object { $alertTypes -contains $_.alertType }
201+
$jsonPRSource = ($alertsPRSource.Content | ConvertFrom-Json).value | Where-Object { $alertTypes -contains $_.alertType }
202+
203+
# Extract alert ids from the list of alerts on pr target/source branch.
204+
$prTargetAlertIds = $jsonPRTarget | Select-Object -ExpandProperty alertId
205+
$prSourceAlertIds = $jsonPRSource | Select-Object -ExpandProperty alertId
206+
207+
# Check for alert ids that are reported in the PR source branch but not the pr target branch
208+
$newAlertIds = Compare-Object $prSourceAlertIds $prTargetAlertIds -PassThru | Where-Object { $_.SideIndicator -eq '<=' }
209+
$dependencyAlerts = $codeAlerts = 0
210+
211+
# Are there any new alert ids in the PR source branch?
212+
if ($newAlertIds.length -gt 0) {
213+
Write-Host "##[warning] The code changes in this PR looks to be introducing new security alerts:"
214+
215+
# Loop over the objects in the prAlerts JSON object
216+
foreach ($prAlert in $jsonPRSource) {
217+
if ($newAlertIds -contains $prAlert.alertId) {
218+
219+
#check to see $alert.severity in $severityPolicy for given alertType - valid severities: https://learn.microsoft.com/en-us/rest/api/azure/devops/advancedsecurity/alerts/list#severity
220+
if ($severityPolicy[$prAlert.alertType] -notcontains $prAlert.severity) {
221+
Write-Host "##[warning] Ignored by policy - $($prAlert.severity) severity $($prAlert.alertType) alert detected #$($prAlert.alertId) : $($prAlert.title) in pr branch $($prSourceBranch)."
222+
continue
223+
}
224+
225+
# New Alert for this PR. Log and report it.
226+
Write-Host ""
227+
if ($prAlert.alertType -eq "dependency") {
228+
Write-Host "##vso[task.logissue type=error] New $($prAlert.severity) severity $($prAlert.alertType) alert detected #$($prAlert.alertId) in library: $($prAlert.logicalLocations[-1].fullyQualifiedName). `"$($prAlert.title)`". Detected in manifest: $($prAlert.physicalLocations[-1].filePath)."
229+
$dependencyAlerts++
230+
}
231+
elseif ($prAlert.alertType -eq "code") {
232+
Write-Host "##vso[task.logissue type=error;sourcepath=$($prAlert.physicalLocations[-1].filePath);linenumber=$($prAlert.physicalLocations[-1].region.lineStart);columnnumber=$($prAlert.physicalLocations[-1].region.columnStart)] New $($prAlert.severity) severity $($prAlert.alertType) alert detected #$($prAlert.alertId) : $($prAlert.title)."
233+
$codeAlerts++
234+
}
235+
236+
Write-Host "##[error] Fix or dismiss this new $($prAlert.alertType) alert in the Advanced Security UI for pr branch $($prSourceBranch)."
237+
$urlAlert = "https://dev.azure.com/{0}/{1}/_git/{2}/alerts/{3}?branch={4}" -f $orgName, $project, $repositoryId, $prAlert.alertId, $prSourceBranch
238+
Write-Host "##[error] Details for this new alert: $($urlAlert)"
239+
AddPRComment $prAlert $urlAlert
240+
}
241+
}
242+
243+
if ($dependencyAlerts + $codeAlerts -gt 0) {
244+
Write-Host
245+
Write-Host "##[error] Dissmiss or fix failing alerts listed (dependency #: $dependencyAlerts / code #: $codeAlerts ) and try re-queue the CIVerify task."
246+
exit 1 #TODO - dynamically pass/fail the build only if a PR comment was added, indicating that there are new alerts that were directly created by this PR. Since we do incremental PR iteration based comments this is not currently viable.
247+
}
248+
else {
249+
Write-Host "##[warning] New alerts detected but none that violate policy - all is fine though these will appear in the Advanced Security UI."
250+
exit 0
251+
}
252+
}
253+
else {
254+
Write-Output "No new alerts - all is fine"
255+
exit 0
256+
}

.azuredevops/appsec/CIVerify.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This is a starter pipeline to handle Gated PRs using GHAzDO. Currently (October 2023) this is not supported out of the box by the product,
2+
# it probably will be supported in the future, this project can be used as a workaround until that time.
3+
# We want to restrict new code going into main and only allow PRs if the new code does not introduce any new CodeQL or Dependency Scanning issues.
4+
# The idea is to set a branch protection policy (for main), forcing this pipeline to succeed before a PR into main can happen.
5+
# The pipeline will run CodeQL and Dependency Scanning on the source branch of the PR. Later, using a PowerShell script, the security alerts of the PR source and target will be compared.
6+
# If there are alerts in the PR source that are not in main, this pipeline will fail.
7+
#
8+
# If new alerts are detected these needs to be analysed using the regular Advanced Security Code Scanning UI.
9+
# - For Code Scanning, use the filter to the pr branch and fix or dissmiss all the new issues.
10+
# - For Depenendcy Scanning, use the the link in the pipeline output or PR comment to view/fix or dissmiss all the new issues.
11+
# After that, the PR check CIVerify can be requeued in the PR. Hopefully this time, without any issues.
12+
#
13+
# The script needs a PAT to run (for accessing the REST API). This PAT should be setup as a secret variable for the pipleline (name: GATING_PAT).
14+
# The PAT needs the access right - Advanced Security - Read
15+
#
16+
# More on ADO build verifications: https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&tabs=browser#build-validation
17+
18+
trigger:
19+
- none
20+
21+
pool:
22+
vmImage: ubuntu-latest
23+
24+
variables:
25+
advancedsecurity.enable: false
26+
advancedsecurity.skip: true
27+
28+
steps:
29+
# Compair CodeQL / Dependency issues on the PR source branch and main.
30+
# Fail if there are new issues.
31+
- task: PowerShell@2
32+
displayName: 'CI Gating - verify there are no new security issues introduced in this PR'
33+
inputs:
34+
targetType: filePath
35+
filePath: .azuredevops/appsec/CIGate.ps1
36+
pwsh: true # Script requires PS7 (otherwise you will see syntax error - Unexpected token '?' )
37+
env:
38+
MAPPED_ADO_PAT: $(System.AccessToken)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
trigger:
2+
- main
3+
4+
schedules:
5+
- cron: 0 0 * * 0
6+
displayName: Weekly Sunday build
7+
branches:
8+
include: [ main ]
9+
always: true
10+
11+
pool:
12+
vmImage: windows-latest
13+
14+
variables:
15+
- group: 'DevSecOps'
16+
- name: advancedsecurity.enable
17+
value: false
18+
- name: advancedsecurity.skip
19+
value: true
20+
21+
steps:
22+
- task: ADOSecurityScanner@1
23+
inputs:
24+
ADOConnectionName: 'sc-ADOSecurityScanner'
25+
OrgName: '$(System.CollectionUri)'
26+
ProjectNames: 'DevSecOps'
27+
ScanFilter: 'All'
28+
BuildNames: '*'
29+
ReleaseNames: '*'
30+
ServiceConnectionNames: '*'
31+
AgentPoolNames: '*'
32+
VariableGroupNames: '*'
33+
isBaseline: false
34+
EnableOMSLogging: true
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
trigger:
2+
batch: true
3+
branches:
4+
include: [main]
5+
6+
pool:
7+
vmImage: ubuntu-latest
8+
9+
variables:
10+
advancedsecurity.enable: false
11+
advancedsecurity.skip: true
12+
13+
stages:
14+
- stage: CI_Build
15+
displayName: 'CI Build'
16+
jobs:
17+
- job: CI_Build
18+
steps:
19+
- checkout: self
20+
- task: DotNetCoreCLI@2
21+
displayName: Restore
22+
inputs:
23+
command: restore
24+
projects: '**/*.csproj'
25+
- task: DotNetCoreCLI@2
26+
displayName: Build
27+
inputs:
28+
projects: '**/*.csproj'
29+
arguments: '--configuration $(BuildConfiguration)'

0 commit comments

Comments
 (0)