Mert Özen Her Satırda Daha İleri
Güvenlik DevSecOps — #07

SBOM ve VEX: CycloneDX 1.7 ve SPDX 3 ile Ne Kullandığını Bilmek

Mert Özen 14 Kas 2026 13 dk 4 görüntülenme
SBOM ve VEX: CycloneDX 1.7 ve SPDX 3 ile Ne Kullandığını Bilmek

SBOM, bir sürümün içindeki bileşenlerin makine okunur listesidir ve asıl değeri yeni bir CVE geldiğinde hangi ürünün hangi sürümünün etkilendiğini build almadan sorgulayabilmektir; VEX ise etkilenip etkilenmediğini gerekçesiyle beyan eder. Bookstore için .NET, Angular ve image SBOM'larını üretip doğruluyor, Dependency-Track'te sorguluyor ve OpenVEX ile CycloneDX VEX'i gerçek çıktılarla uyguluyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; SBOM'lar (yazılımın içerdiği bileşenlerin listesi) CycloneDX .NET 6.3.1, Microsoft sbom-tool 4.1.5, @cyclonedx/cyclonedx-npm 7.0.1 (npm 11.19) ve syft 1.57.1 ile üretilmiş; Dependency-Track 5.2.1, vexctl 0.4.4, Trivy 0.76.2 ve Grype 0.121.3 ile 13 Kasım 2026 tarihli zafiyet verileriyle işlenmiştir.

Önce Tahmin Et

Bir sabah lodash'in template fonksiyonuyla ilgili bir advisory gündeme düşüyor ve ürün sahibi soruyor: "Müşteride çalışan Bookstore 1.4.0 bundan etkileniyor mu?" 1.4.0'ın tag'i duruyor; repo'yu checkout edip npm ls çalıştırabilirsin. Peki production'daki image'da hangi ASP.NET Core runtime sürümü var? Bunu proje dosyasından okuyabilir misin? Ve cevap "paket var ama o fonksiyonu çağırmıyoruz" ise, bunu her tarama raporunda yeniden açıklamak zorunda mısın?

Bu yazıda SBOM ve VEX (bir zafiyetin üründe gerçekten istismar edilebilir olup olmadığını bildiren doküman) ile bu sorulara dakikalar içinde cevap veren bir düzen kuruyoruz: SBOM'u .NET, Angular ve container için üretmek, aracın çıktısının gerçekten doğru olduğunu kontrol etmek, SBOM'u artifact'la birlikte saklamak, Dependency-Track ile sorgulanabilir bir envantere çevirmek ve "etkilenmiyoruz, çünkü..." cümlesini makine okunur bir beyana dönüştürmek.

SBOM Ne İşe Yarar ve Hangi Format

SBOM, bir yazılım sürümünün içindeki bileşenlerin listesidir: paket adı, sürümü, PURL (paketi ekosistem, ad ve sürümle tanımlayan URL formatı) gibi bir tanımlayıcı, hash'i, lisansı ve bileşenler arasındaki bağımlılık ilişkileri. OWASP (web uygulama güvenliği üzerine çalışan açık topluluk) serisinin "OWASP A03: Software Supply Chain Failures — NuGetAudit, npm audit ve SRI" yazısı SBOM'a giriş yaptı; burada amacı daha net koyalım: SBOM bir uyum belgesi değil, bir sorgu kaynağı. Değeri, yeni bir CVE (yayımlanmış güvenlik açıklarının standart kimlik numarası) yayımlandığında "hangi ürünümüzün hangi sürümünde bu paket var?" sorusuna build almadan, image çekmeden cevap verebilmekte.

İki yaygın format var, CycloneDX ve SPDX (yazılım bileşen ve lisans bilgisi için SBOM formatı), ve ikisi de makine okunur:

CycloneDX 1.7SPDX 3.0.1
ArkasındaOWASP; 1.6 sürümü ECMA-424 olarak standartlaştıLinux Foundation; 2.2.1 sürümü ISO/IEC 5962:2021
Çıkış noktasıGüvenlik ve supply chainLisans uyumu, sonra genel amaçlı bir model
Zafiyet ve VEXAynı belgede vulnerabilities ve analysisSecurity profile ile
Yeni olanEkim 2025: citation, patent, kriptografi kataloğuJSON-LD (birbirine bağlı veriler için JSON formatı) graph, profile'lar (Software, Security, Build, AI, Dataset)
Bookstore'daCycloneDX .NET, cyclonedx-npm, syftMicrosoft sbom-tool

Pratik kural: zafiyet yönetimi ve VEX için CycloneDX'in araç ekosistemi bugün daha geniş; bir müşteri ya da kurum SPDX istiyorsa sbom-tool ya da syft ile ikisini birden üretmek ucuz. Hangisini seçersen seç, tek bir formatta tutarlı kalmak, iki formatı yarım yamalak tutmaktan iyidir.

.NET Tarafı: CycloneDX .NET ve sbom-tool

CycloneDX'in .NET aracı proje dosyasından (ya da solution'dan) restore edilmiş paket graph'ını okur. Aracı global değil, repo'daki bir klasöre kuruyorum:

dotnet tool install CycloneDX --version 6.3.1 --tool-path .tools
.tools/dotnet-CycloneDX src/Bookstore.Api/Bookstore.Api.csproj -o sbom -fn bookstore-api.cdx.json -F Json -sv 1.4.0
Found 27 packages

Creating CycloneDX BOM

Varsayılan format CycloneDX 1.7. 27 paketin arasında SqlClient 5.1.1'in transitive olarak getirdiği Azure.Identity 1.7.0 da var; her biri PURL, SHA-512 (Secure Hash Algorithm) hash ve lisansla geliyor. Bir şey eksik: ASP.NET Core'un kendisi. Uygulama bir framework reference ile çalıştığı için shared framework bir NuGet paketi değil ve proje SBOM'unda görünmüyor. Runtime sürümünü image SBOM'undan alacağız.

Microsoft'un sbom-tool'u farklı bir soruya cevap veriyor: build çıktısında hangi dosyalar var ve hash'leri ne? Publish klasörünü ve kaynak kodu verip SPDX 3.0 istedim:

dotnet publish src/Bookstore.Api -c Release -o out/api
sbom-tool generate -b out/api -bc src/Bookstore.Api -pn Bookstore.Api -pv 1.4.0 -ps Bookstore \
  -nsb https://sbom.bookstore.example -mi SPDX:3.0

Üç saniyede out/api/_manifest/spdx_3.0/manifest.spdx.json oluştu: SPDX 3.0.1 context'li bir JSON-LD graph, içinde 28 paket, publish klasöründeki 46 dosyanın her biri hash'iyle ve 230 relationship. Bu dosya listesi, "müşteriye giden zip'te ne vardı?" sorusunun cevabı; CI/CD Temelleri serisinin "Build Once, Deploy Many: Artifact, Versiyonlama ve Immutable Paket" yazısındaki immutable artifact'ın içerik kanıtı gibi düşün.

Angular Tarafı: npm sbom'un Saymadıkları

npm'in kendi npm sbom komutu ilk akla gelen. Production bağımlılıklarını istedim:

npm sbom --sbom-format cyclonedx --omit dev | jq -r '.components[] | "\(.name)@\(.version)"'
@angular/forms@22.2.6
@angular/router@22.2.6
@standard-schema/spec@1.1.0
lodash-es@4.17.20

Dört paket. @angular/core, rxjs ve tslib yok; oysa uygulama bunlar olmadan açılmaz. Aynı ağacı npm'in kendi query'siyle saydım: npm query .prod | jq length kök paket dahil 12 döndü. npm 11.19.1'de bu komut, development bağımlılıklarının da kullandığı production paketlerini --omit dev ile birlikte dışarıda bıraktı. Aynı node_modules üzerinde CycloneDX'in npm aracını çalıştırdım:

npx --yes @cyclonedx/cyclonedx-npm@7.0.1 --omit dev --spec-version 1.7 \
  --output-format JSON --output-file ../sbom/bookstore-web.cdx.json
@angular/common@22.2.6
@angular/compiler@22.2.6
@angular/core@22.2.6
@angular/forms@22.2.6
@angular/platform-browser@22.2.6
@angular/router@22.2.6
lodash-es@4.17.20
rxjs@7.8.2
tslib@2.9.1
@standard-schema/spec@1.1.0
zod@4.6.6

On bir paket, npm query'nin listesiyle birebir. Ders aracın adı değil: SBOM'u bir kez, bağımsız bir kaynakla karşılaştırarak doğrula. Eksik bir SBOM, hiç olmayan bir SBOM'dan daha tehlikeli, çünkü "listede yok, demek ki etkilenmiyoruz" cevabını güvenle verdirir. npm ci'yi --ignore-scripts ile çalıştırmanın nedeni Frontend Güvenliği serisinin "npm Supply Chain'i Sertleştirmek" yazısında.

Image SBOM'u ve Pipeline'da Saklamak

Deploy edilen şey image olduğu için asıl envanter image'ın SBOM'u. Geçen yazıdaki chiseled Dockerfile'la build ettiğim bookstore-api:1.4.0'ı syft ile taradım:

syft scan docker:bookstore-api:1.4.0 -o cyclonedx-json=sbom/bookstore-api-image.cdx.json
jq -r '.components[] | (.purl // .type) | split("/")[0]' sbom/bookstore-api-image.cdx.json | sort | uniq -c | sort -rn
 351 file
  29 pkg:nuget
   9 pkg:deb
   1 operating-system

29 NuGet paketinin 27'si proje SBOM'undakilerle aynı; fazladan ikisi Microsoft.AspNetCore.App.Runtime.linux-x64 ve Microsoft.NETCore.App.Runtime.linux-x64, ikisi de 10.0.15. Bir .NET runtime advisory'si yayımlandığında cevap bu satırlarda. Dokuz deb paketi chiseled image'ın Ubuntu parçaları; 351 dosya ise syft'in hash'lediği runtime ve uygulama dosyaları.

SBOM'u ne zaman ürettiğin, nerede ürettiğin kadar önemli: aynı build'in çıktısından üretilmeli ve aynı artifact'la saklanmalı. Sonradan "1.4.0 tag'ini checkout edip SBOM üretmek" farklı bir restore, farklı bir base image digest'i ve dolayısıyla farklı bir liste verebilir. Bookstore'un main workflow'undaki SBOM job'ı:

name: sbom

on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  sbom:
    runs-on: ubuntu-latest
    env:
      VERSION: 1.4.${{ github.run_number }}
      SYFT_VERSION: 1.57.1
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0
        with:
          dotnet-version: 10.0.x
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24
      - uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3
      - name: Install SBOM tools
        run: |
          dotnet tool install CycloneDX --version 6.3.1 --tool-path .tools
          base="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}"
          for f in checksums.txt checksums.txt.sigstore.json linux_amd64.tar.gz; do
            curl -sSfLO "$base/syft_${SYFT_VERSION}_$f"
          done
          cosign verify-blob "syft_${SYFT_VERSION}_checksums.txt" \
            --bundle "syft_${SYFT_VERSION}_checksums.txt.sigstore.json" \
            --certificate-oidc-issuer https://token.actions.githubusercontent.com \
            --certificate-identity-regexp '^https://github.com/anchore/syft/'
          sha256sum --ignore-missing -c "syft_${SYFT_VERSION}_checksums.txt"
          tar -xzf "syft_${SYFT_VERSION}_linux_amd64.tar.gz" -C .tools syft
      - name: SBOM for the API project
        run: .tools/dotnet-CycloneDX src/Bookstore.Api/Bookstore.Api.csproj -o sbom -fn bookstore-api.cdx.json -F Json -sv "$VERSION"
      - name: SBOM for the web app
        working-directory: web
        run: |
          npm ci --ignore-scripts
          npx --yes @cyclonedx/cyclonedx-npm@7.0.1 --omit dev --spec-version 1.7 \
            --output-format JSON --output-file ../sbom/bookstore-web.cdx.json
      - name: SBOM for the image
        run: |
          docker build -t "bookstore-api:$VERSION" .
          .tools/syft scan "docker:bookstore-api:$VERSION" -o "cyclonedx-json=sbom/bookstore-api-image.cdx.json"
      - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: sbom-${{ env.VERSION }}
          path: sbom/
      - name: Upload to Dependency-Track
        env:
          DT_URL: ${{ vars.DEPENDENCY_TRACK_URL }}
          DT_API_KEY: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
        run: |
          for p in bookstore-api:sbom/bookstore-api-image.cdx.json bookstore-web:sbom/bookstore-web.cdx.json; do
            curl -sSf -X POST "$DT_URL/api/v1/bom" -H "X-Api-Key: $DT_API_KEY" \
              -F autoCreate=true -F "projectName=${p%%:*}" -F "projectVersion=$VERSION" -F "bom=@${p#*:}"
          done

Workflow'u actionlint 1.7.12 ile doğruladım. Adımları yerelde macOS karşılıklarıyla tek tek çalıştırdım: syft'in checksum dosyasının Sigstore imzası cosign'da Verified OK verdi, upload döngüsü yerel Dependency-Track'e iki SBOM'u da yükledi. syft binary'sini geçen yazıdaki Trivy gibi imzasıyla doğruluyoruz; SBOM üreten araç da supply chain'in parçası. Artifact'ın saklama süresi, müşteride desteklediğin en eski sürüm kadar uzun olmalı; GitHub artifact'larının varsayılan süresi bunun için kısa kalıyorsa SBOM'u release asset'i olarak ya da registry'de image'ın yanında sakla. Sekizinci yazıda SBOM'u image'a imzalı bir attestation olarak bağlayacağız.

Dependency-Track: SBOM'u Sorgulanabilir Envantere Çevirmek

SBOM'u bir artifact klasöründe bırakmak, kütüphaneyi kataloğu olmadan rafa dizmeye benzer. Dependency-Track, SBOM'ları proje ve sürüm bazında saklayan, zafiyet kaynaklarıyla sürekli eşleştiren açık kaynak bir OWASP projesi. 5.2.1 sürümünü bir PostgreSQL ile iki container olarak yerelde çalıştırdım. İlk açılışta NVD'yi (ABD'nin ulusal zafiyet veritabanı) 3 dakika 36 saniyede, 440.922 kayıtla mirror'ladı; EPSS (bir zafiyetin istismar edilme olasılığını tahmin eden skor), CISA (Cybersecurity and Infrastructure Security Agency) KEV (CISA'nın aktif olarak istismar edilen zafiyetler listesi) listesi ve ENISA (European Union Agency for Cybersecurity) kaynaklı KEV verisi de varsayılan olarak geldi. npm ve NuGet paketleri için OSV'yi (açık kaynak zafiyet veritabanı) ayrıca açtım; iki ekosistemin mirror'ı 50 saniye sürdü.

Pipeline'daki upload adımının yaptığı şey bu:

curl -s -X POST "$DT_URL/api/v1/bom" -H "X-Api-Key: $DT_API_KEY" \
  -F autoCreate=true -F projectName=bookstore-web -F projectVersion=1.4.0 \
  -F "bom=@sbom/bookstore-web.cdx.json"
{"token":"01a1d71d-6225-7409-b1b4-d2db06819d9e","projectUuid":"1a9c7239-a8aa-4306-8d5e-809059fa2fd1"}

Yükleme asenkron; token ile işlemin bitip bitmediğini sorabiliyorsun. API key'i yalnız BOM_UPLOAD, PROJECT_CREATION_UPLOAD ve VIEW_PORTFOLIO izinli "Automation" team'ine ait. Dependency-Track 5'in API'sı bunun yerine GitHub Actions'ın OIDC (OpenID Connect) token'ına güvenen workload identity binding'leri de tanımlıyor; uzun ömürlü bir API key saklamamak için onu tercih et (bunu yerelde deneyemedim, yalnız API şemasında gördüm).

Şimdi açılıştaki soru. Advisory'nin GHSA (GitHub Security Advisory) kimliğiyle portföyü sorguladım:

curl -s -H "X-Api-Key: $DT_API_KEY" \
  "$DT_URL/api/v1/vulnerability/source/GITHUB/vuln/GHSA-35jh-r3h4-6jhm/projects" | jq -r '.[] | "\(.name) \(.version)"'
curl -s -H "X-Api-Key: $DT_API_KEY" \
  "$DT_URL/api/v1/component/identity?purl=pkg:nuget/Microsoft.AspNetCore.App.Runtime.linux-x64" \
  | jq -r '.[] | "\(.name) \(.version) \(.project.name) \(.project.version)"'
bookstore-web 1.4.0
Microsoft.AspNetCore.App.Runtime.linux-x64 10.0.15 bookstore-api 1.4.0

İlk sorgu 37 milisaniye sürdü. İkincisi bir zafiyet kimliğine değil bir paketin kimliğine bakıyor: henüz CVE'si olmayan, yalnız "şu sürümler etkileniyor" diye duyurulan bir sorunda da çalışır.

07-sbom-flow-tr.png

Bir uyarı. Image projesi için Dependency-Track 49 bulgu gösterdi: dördü NuGet paketlerinde, 45'i Ubuntu'nun openssl paketinde ve 45'inin kaynağı da NVD. Aynı SBOM'u Grype ile taradığımda openssl için beş medium bulgu çıktı. Fark eşleştirme yönteminde: NVD'nin CPE (yazılım ve platformları adlandırma standardı) eşleşmesi upstream sürüme (3.0.13) bakıyor, Ubuntu'nun aynı sürüm numarası altında backport ettiği düzeltmeleri bilmiyor; Grype ise dağıtımın kendi güvenlik verisini kullanıyor. Envanter aracının sonucunu, hangi veri kaynağıyla eşleştiğini görmeden gate'e bağlama; işletim sistemi paketleri için dağıtım verisini kullanan bir tarayıcı, uygulama paketleri için OSV ya da GitHub Advisory Database daha isabetli.

VEX: "Etkilenmiyoruz, Çünkü..."

Üçüncü yazıda reachability analizi yapmıştık: Bookstore'un web tarafı lodash-es'ten yalnız debounce'u import ediyor, template, unset ya da omit çağrılmıyor; SqlClient'ın Azure.Identity'yi kullandığı Entra ID authentication yolu da SQL login kullandığımız için çalışmıyor. Bu kararların yeri bir wiki sayfası ya da bir tarayıcının ignore dosyası değil. VEX, bir ürünün belirli bir sürümünün belirli bir zafiyetten etkilenip etkilenmediğini, gerekçesiyle birlikte makine okunur biçimde söyleyen belgedir. Dört durum var: not_affected, affected, fixed ve under_investigation; not_affected için gerekçe sabit bir listeden seçiliyor.

OpenVEX formatında belgeyi vexctl ile oluşturdum:

vexctl create --file vex/bookstore-web-1.4.0.openvex.json --author "Bookstore Security Champions" \
  --product "pkg:npm/bookstore-web@1.4.0" --subcomponents "pkg:npm/lodash-es@4.17.20" \
  --vuln CVE-2021-23337 --aliases GHSA-35jh-r3h4-6jhm --status not_affected \
  --justification vulnerable_code_not_in_execute_path \
  --impact-statement "Bookstore web imports only debounce from lodash-es; _.template is never called."

Kalan üç lodash bulgusunu vexctl add ile ekledim; ReDoS (Regular Expression Denial of Service) bulgusu için gerekçe farklı: debounce zafiyetli toNumber'ı çağırıyor ama ona yalnız sabit 300 değeri gidiyor, yani vulnerable_code_cannot_be_controlled_by_adversary. Belgeyi Trivy'ye verdim:

trivy sbom -f json --vex vex/bookstore-web-1.4.0.openvex.json --show-suppressed \
  sbom/bookstore-web.cdx.json -o trivy-web-vex.json
jq -r '.Results[] | "vulnerabilities: \(.Vulnerabilities // [] | length)",
  (.ExperimentalModifiedFindings // [] | .[] | "\(.Status) \(.Finding.VulnerabilityID) \(.Statement)")' trivy-web-vex.json
vulnerabilities: 0
not_affected CVE-2021-23337 vulnerable_code_not_in_execute_path
not_affected CVE-2026-4800 vulnerable_code_not_in_execute_path
not_affected CVE-2020-28500 vulnerable_code_cannot_be_controlled_by_adversary
not_affected CVE-2025-13465 vulnerable_code_not_in_execute_path

Raporda açık bulgu kalmadı; dördü de gerekçesiyle "suppressed" olarak listelendi. Aynı belgeyi Grype'a --vex ile verdiğimde ise dört bulgu olduğu gibi kaldı. Grype'ın dokümanı VEX'teki product'ı bir container image (pkg:oci ve digest) olarak eşliyor; benim product'ım bir npm paketiydi. VEX'in product kimliği, onu okuyacak aracın eşleme kuralına uymak zorunda; bunu bir kez gerçekten çalıştırarak doğrula.

Dependency-Track için CycloneDX formatında bir VEX yazdım; bu belgede bulgular analysis alanıyla ve etkilenen bileşene bom-ref ile bağlanıyor:

{
  "id": "GHSA-5mfx-4wcx-rv27",
  "source": {
    "name": "GITHUB",
    "url": "https://github.com/advisories/GHSA-5mfx-4wcx-rv27"
  },
  "analysis": {
    "state": "not_affected",
    "justification": "code_not_reachable",
    "detail": "SQL authentication only; the Entra ID authentication paths in SqlClient that load Azure.Identity are never used."
  },
  "affects": [
    {
      "ref": "pkg:nuget/Azure.Identity@1.7.0?package-id=512f2c7dd50681fb"
    }
  ]
}

İlk yüklemede hiçbir şey değişmedi ve sunucu log'unda dört kez Unable to locate affected element ... based on the BOM reference uyarısı vardı. Dependency-Track, affects'teki referansı VEX belgesinin kendi components listesinde arıyor; iki bileşeni aynı bom-ref'lerle belgeye ekleyince eşleşti. SqlClient bulgusunu ise exploitable durumunda, update response'u ve "1.5.0'da SqlClient 7'ye yükseltiliyor" notuyla yazdım; VEX yalnız "etkilenmiyoruz" demek için değil, "etkileniyoruz ve planımız bu" demek için de var:

Azure.Identity GHSA-m5vv-6r4h-3vj9 NOT_AFFECTED suppressed=true
Azure.Identity GHSA-5mfx-4wcx-rv27 NOT_AFFECTED suppressed=true
Azure.Identity GHSA-wvxc-855f-jvrv NOT_AFFECTED suppressed=true
Microsoft.Data.SqlClient GHSA-98g6-xh36-x2p7 EXPLOITABLE suppressed=false

İki ayrıntı daha çıktı. Automation team'inin API key'iyle VEX yüklemek 403 döndü; VEX bir analiz kararı olduğu için ayrı bir izin (VULNERABILITY_ANALYSIS) gerekiyor ve bu izni SBOM yükleyen key'e vermek yerine kararı veren ekibe bırakmak doğru. İkincisi, aynı SBOM'u 1.4.1 olarak yükleyince yeni sürümdeki dört bulgunun hiçbirinde analiz yoktu. VEX bir sürüme ait; bu yüzden belgeler repo'da, ürünle birlikte versiyonlanıyor ve her sürümde yeniden uygulanıyor.

Regülasyon: EU Cyber Resilience Act

Avrupa Birliği pazarına dijital ürün sunan üreticiler için SBOM artık bir iyi pratikten fazlası. Regulation (EU) 2024/2847, yani CRA (AB'nin dijital ürünlerde siber güvenlik yasası), 10 Aralık 2024'te yürürlüğe girdi. Aktif olarak sömürülen zafiyetlerin ve ciddi olayların raporlanması (Madde 14) 11 Eylül 2026'dan beri uygulanıyor; ürün tasarımına dair ana yükümlülükler 11 Aralık 2027'de başlıyor. Ek I'deki zafiyet yönetimi gereklilikleri, üreticinin üründeki bileşenleri yaygın ve makine okunur bir formatta, en az üst seviye bağımlılıkları kapsayan bir SBOM ile belgelemesini istiyor; SBOM'un kamuya açık olması gerekmiyor, ama teknik dokümantasyonun parçası olarak piyasa gözetim otoritesinin gerekçeli talebinde sunulabilmeli. Tarihleri İrlanda'nın ulusal siber güvenlik merkezinin CRA sayfası ve birkaç hukuk bürosunun özetiyle karşılaştırdım; uygulamadan önce EUR-Lex'teki resmî metne bak. Bu paragraf hukuki tavsiye değil, kendi ürünün için hukuk ekibinle çalış. Teknik açıdan çıkarım basit: Bookstore'un her sürümü için SBOM'u üretip artifact'la saklayan pipeline, bu yükümlülüğün mühendislik tarafını zaten karşılıyor.

Üretimde Dikkat

SBOM'un kalitesini ölç. Bileşen sayısını bağımsız bir kaynakla (npm query, dotnet list package --include-transitive) karşılaştıran küçük bir kontrol, eksik SBOM'un sessizce üretilmesini engeller. PURL'u olmayan bileşen oranı da iyi bir sinyal: PURL'suz bir bileşen hiçbir advisory ile eşleşmez.

Envanteri güncel tut. Dependency-Track'te production'da artık çalışmayan sürümleri pasife al; aksi hâlde yeni bir CVE geldiğinde "etkilenen" listesi, kimsenin kullanmadığı sürümlerle dolar ve gerçek cevabı gizler.

VEX'in sahibi olsun. Her not_affected beyanının arkasında bir reachability gerekçesi ve onu yazan biri olmalı; kod değiştiğinde (biri template'i import ettiğinde) beyan yanlış hâle gelir. Üçüncü yazıdaki süreli istisnalar gibi VEX belgelerini de code review'dan geçir ve dokuzuncu yazıdaki istisna metriklerine dahil et.

SBOM'u müşteriyle paylaşacaksan içeriğine bak: dahili paket adları, registry URL'leri ve dosya yolları bilgi sızdırabilir. CycloneDX araçlarının filtre seçenekleri bunun için var.

Sık Düşülen Tuzaklar

Birincisi, aracın çıktısını doğrulamadan SBOM'a güvenmek. Bu yazıda npm sbom --omit dev, on bir production paketinin yalnız dördünü listeledi.

İkincisi, yalnız proje SBOM'u üretmek. Runtime, base image ve işletim sistemi paketleri proje dosyasında yok; deploy edilen image'ın SBOM'u olmadan .NET runtime advisory'sine cevap veremezsin.

Üçüncüsü, SBOM'u sonradan, kaynaktan yeniden üretmek. Aynı tag farklı bir restore ve farklı bir base image ile farklı bir liste verir; SBOM build'in çıktısıdır.

Dördüncüsü, VEX'i tarayıcının ignore dosyasıyla karıştırmak. Ignore dosyası bir aracın davranışını değiştirir; VEX ise ürün hakkında, gerekçesiyle ve sürüme bağlı bir beyandır ve müşteriyle paylaşılabilir.

Beşincisi, envanter aracındaki bulgu sayısını veri kaynağını bilmeden yorumlamak. 45 openssl bulgusunun hepsi NVD'nin upstream sürüm eşleşmesinden geldi.

Küçük Bir Deneme

★☆☆ Projenin web tarafı için npm sbom --omit dev ve @cyclonedx/cyclonedx-npm --omit dev çıktılarını üret, bileşen sayılarını npm query .prod ile karşılaştır.

★★☆ Uygulamanın proje SBOM'unu ve image SBOM'unu üret. Image'da olup projede olmayan bileşenleri listele; runtime sürümünü bul.

★★★ Yerel bir Dependency-Track'e iki sürümün SBOM'unu yükle, bir bulgu için CycloneDX VEX yazıp birinci sürüme uygula. İkinci sürümde analizin olmadığını gör, sonra VEX'i pipeline'ında her sürümde uygulayan bir adım tasarla.

Artık ne kullandığımızı biliyoruz. Ama bir müşteri ya da Kubernetes cluster'ı, eline gelen image'ın ve SBOM'un gerçekten Bookstore'un pipeline'ından çıktığını nereden bilecek? Bir sonraki yazıda Sigstore ile keyless imzalamaya, GitHub artifact attestation'larına ve SLSA (Supply-chain Levels for Software Artifacts) v1.2'ye bakıyoruz.