Azure DevOps Pipeline’larında Otomatik NuGet Zafiyet Taraması (DevSecOps)
Yazılım projelerimizde kullandığımız üçüncü parti kütüphanelerin güvenliği, uygulamamızın genel sağlığı için hayati önem taşır. CI/CD süreçlerimize entegre edeceğimiz otomatik güvenlik taramaları sayesinde, olası zafiyetleri henüz production ortamına çıkmadan yakalayabiliriz. Bu yazıda, Azure DevOps pipeline’larınıza NuGet paketleri için nasıl otomatik zafiyet taraması (vulnerability scan) ekleyebileceğinizi adım adım anlatacağım.
Zafiyet Tarama Şablonunun Oluşturulması
İlk adım olarak, tarama işlemini üstlenecek ve projelerimizde tekrar tekrar kullanabileceğimiz bir YAML şablonu (template) hazırlıyoruz. Bu şablon arka planda bir PowerShell betiği çalıştırarak şu adımları gerçekleştirir:
- Çalışma dizinindeki (checkout dizini) tüm
.csprojdosyalarını bularak tarama kapsamına alır. - Her bir proje için
dotnet list package --vulnerable --include-transitivekomutunu çalıştırarak hem doğrudan kurulan hem de dolaylı (transitive) gelen paketleri denetler. - Belirlediğiniz eşik değerine (varsayılan olarak Critical ve High) uyan bir zafiyet tespit edilirse pipeline’ı “Fail” (hata) durumuna çeker.
- Transitive paketlerdeki zafiyetler için
dotnet nuget whykomutunu kullanarak, hangi üst düzey (top-level) paketin bu zafiyetli kütüphaneyi getirdiğini loglarda detaylıca raporlar.
Aşağıdaki içeriği templates/nuget-vulnerability-scan.yml adıyla kaydedin:
# templates/nuget-vulnerability-scan.yml
parameters:
- name: workingDirectory
type: string
default: '$(System.DefaultWorkingDirectory)'
- name: severityThreshold
type: string
default: 'Critical,High'
- name: failOnNotFound
type: boolean
default: false
steps:
- task: PowerShell@2
displayName: 'Scan and fail on vulnerable NuGet packages'
inputs:
targetType: 'inline'
script: |
$workingDir = "${{ parameters.workingDirectory }}"
$severityList = "${{ parameters.severityThreshold }}" -split ',' | ForEach-Object { $_.Trim() }
$failOnNotFound = [System.Convert]::ToBoolean("${{ parameters.failOnNotFound }}")
Write-Host "Tarama dizini : $workingDir"
Write-Host "Fail eden seviyeler: $($severityList -join ', ')"
$projects = Get-ChildItem -Path $workingDir -Recurse -Filter "*.csproj" -ErrorAction SilentlyContinue
if (-not $projects -or $projects.Count -eq 0) {
$msg = "Hic csproj dosyasi bulunamadi: $workingDir"
if ($failOnNotFound) {
Write-Host "##vso[task.logissue type=error]$msg"
exit 1
} else {
Write-Host "##vso[task.logissue type=warning]$msg"
exit 0
}
}
Write-Host "Taranacak proje sayisi: $($projects.Count)"
$projects | ForEach-Object { Write-Host " - $($_.FullName)" }
$findings = @()
foreach ($proj in $projects) {
Write-Host "Taraniyor: $($proj.FullName)"
$vulnJsonPath = Join-Path $env:AGENT_TEMPDIRECTORY "vuln_$($proj.BaseName)_$([guid]::NewGuid().ToString('N')).json"
dotnet list "$($proj.FullName)" package --vulnerable --include-transitive --format json | Out-File $vulnJsonPath -Encoding utf8
$content = Get-Content $vulnJsonPath -Raw -ErrorAction SilentlyContinue
if (-not $content) { continue }
try { $json = $content | ConvertFrom-Json } catch {
Write-Host "##[warning]$($proj.Name) icin JSON parse edilemedi, atlaniyor."
continue
}
if (-not $json.projects) { continue }
foreach ($project in $json.projects) {
foreach ($fw in $project.frameworks) {
foreach ($pkg in @($fw.topLevelPackages)) {
if ($pkg.vulnerabilities) {
foreach ($v in $pkg.vulnerabilities) {
if ($v.severity -in $severityList) {
$findings += [pscustomobject]@{
Project = $proj.Name
Package = $pkg.id
Resolved = $pkg.resolvedVersion
Severity = $v.severity
Advisory = $v.advisoryurl
Type = 'TopLevel'
UpdateHint = "PackageReference'i direkt guncelleyin: $($pkg.id)"
}
}
}
}
}
foreach ($pkg in @($fw.transitivePackages)) {
if ($pkg.vulnerabilities) {
foreach ($v in $pkg.vulnerabilities) {
if ($v.severity -in $severityList) {
$whyOutput = dotnet nuget why "$($proj.FullName)" "$($pkg.id)" 2>&1 | Out-String
$findings += [pscustomobject]@{
Project = $proj.Name
Package = $pkg.id
Resolved = $pkg.resolvedVersion
Severity = $v.severity
Advisory = $v.advisoryurl
Type = 'Transitive'
UpdateHint = $whyOutput.Trim()
}
}
}
}
}
}
}
}
if ($findings.Count -gt 0) {
Write-Host "##vso[task.logissue type=error]NuGet guvenlik zaafiyeti bulundu ($($severityList -join '/'))!"
Write-Host ""
foreach ($item in $findings) {
Write-Host "============================================================"
Write-Host "Proje : $($item.Project)"
Write-Host "Paket : $($item.Package) ($($item.Resolved))"
Write-Host "Seviye : $($item.Severity)"
Write-Host "Advisory : $($item.Advisory)"
Write-Host "Tip : $($item.Type)"
if ($item.Type -eq 'Transitive') {
Write-Host "--- Bagimlilik zinciri (hangi paketi guncellemelisiniz) ---"
Write-Host $item.UpdateHint
} else {
Write-Host "Aksiyon : $($item.UpdateHint)"
}
Write-Host ""
}
exit 1
} else {
Write-Host "Belirtilen seviyede ($($severityList -join '/')) zafiyet bulunamadi."
}
(Not: Bu işlemin düzgün çalışabilmesi için pipeline agent’ınızda .NET SDK 8.0.4 veya üzeri bir sürümün kurulu olması ve UseDotNet@2 adımının bu betikten önce çalıştırılmış olması gerekmektedir.)
Ana Pipeline’a Entegrasyon Senaryoları
Şablonu oluşturduktan sonra, projelerinizin ana azure-pipelines.yml dosyasında bu şablonu nasıl çağıracağınıza karar vermelisiniz. İhtiyacınıza göre iki farklı senaryo kullanabilirsiniz:
Şablon Aynı Repo İçerisindeyse Eğer projeniz monorepo yapısındaysa veya şablonu sadece o proje için kullanacaksanız, şablonu doğrudan yolunu belirterek çağırabilirsiniz:
steps:
- task: NuGetCommand@2
name: 'NugetUpdate'
inputs:
restoreSolution: '${{ parameters.solutionFileName }}'
feedsToUse: 'config'
continueOnError: true
includeNuGetOrg: true
- template: templates/nuget-vulnerability-scan.yml
- task: DotNetCoreCLI@2
displayName: 'Publish Project'
condition: and(succeeded(), ne(${{ parameters.createImage }}, true))
inputs:
command: publish
projects: '**/${{ parameters.ProjectFileName }}'
publishWebProjects: False
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: True
Bu yapı sayesinde tek bir satır kod ile tüm uygulamalarınızı bilinen güvenlik zafiyetlerine karşı tarayabilir, geliştirme süreçlerinizi çok daha güvenli hale getirebilirsiniz!