Mert Özen Her Satırda Daha İleri
Test ve Kalite Statik Analiz — #02

.NET Analyzer'ları: AnalysisLevel, CA Kuralları, .globalconfig, WarningsAsErrors ve Legacy Projede Kademeli Açmak

Mert Özen 14 Kas 2026 15 dk 11 görüntülenme
.NET Analyzer'ları: AnalysisLevel, CA Kuralları, .globalconfig, WarningsAsErrors ve Legacy Projede Kademeli Açmak

AnalysisLevel'ı 10.0-recommended gibi bir sürüme sabitlemek, SDK yükseltmesinin kendiliğinden yeni kural açıp CI'ı kırmasını önler; Bookstore'da .NET 11 SDK'sı latest ile CA2028 hatası verdi, sabit seviyede temiz geçti. Yazı .globalconfig ile .editorconfig'in rolünü, Türkçe i ile kupon eşleşmeyen CA1309'u, SQL injection'ı gösteren CA2100'ü, hedefli WarningsAsErrors'ı, gerekçeli bastırmayı ve SARIF ile PR annotation'ını gerçek çıktılarla anlatıyor.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.402) ile derlenip denenmiştir; SDK yükseltme deneyi .NET 11 SDK 11.0.100 ile, bastırma kontrolü StyleCop.Analyzers 1.3.180 ile yapılmış, uygulama SQL Server 2025'e karşı çalıştırılmıştır. GitHub Actions workflow'u actionlint 1.7.12 ile doğrulanmış, GitHub'da çalıştırılmamıştır.

Önce Tahmin Et

İlk yazıda sitedeki serilerin ortak örnek uygulaması Bookstore'a (bir online kitapçı) fiyat içe aktarma endpoint'i eklemiş ve varsayılan analyzer ayarlarının yedi diagnostic'in beşini göstermediğini görmüştük. Ekip ders alıyor: Directory.Build.props'a <AnalysisLevel>latest-recommended</AnalysisLevel> ekleniyor, çıkan uyarılar temizleniyor, TreatWarningsAsErrors zaten açık. Bu arada içe aktarmaya, ISBN (kitapların uluslararası standart numarası) değerlerini, "ISBN 978-0-441-17271-9" gibi yazılmış olsalar da normalize eden küçük bir yardımcı da ekleniyor:

using System.Text.RegularExpressions;

namespace Bookstore.Catalog;

public static class Isbn
{
    private static readonly Regex Isbn13Pattern = new(
        @"^(?:ISBN[- ]?)?(97[89])[- ]?(\d{1,5})[- ]?(\d+)[- ]?(\d+)[- ]?(\d)$", RegexOptions.Compiled);

    // "ISBN 978-0-441-17271-9" değerini "9780441172719"a çevirir; geri kalan her şey için null döner.
    public static string? Normalize(string input)
    {
        string candidate = input.Trim();
        if (Isbn13Pattern.IsMatch(candidate))
        {
            Match match = Isbn13Pattern.Match(candidate);
            string digits = string.Concat(match.Groups.Values.Skip(1).Select(g => g.Value));
            return digits.Length == 13 ? digits : null;
        }

        return null;
    }
}

Build temiz, CI yeşil. Bir ay sonra .NET 11 çıkıyor ve Emre tek satırlık bir PR (pull request; değişikliğin ana branch'e alınması için açılan inceleme isteği) açıyor: global.json'daki SDK sürümü 10.0.100'den 11.0.100'e. Kodda hiçbir değişiklik yok, proje hâlâ net10.0 hedefliyor. CI ne der? Ve bu PR'ın hiçbir kod değişikliği olmadan kırmızı yanmaması için hangi tek satır yeterdi?

SDK'nın Analyzer'ları ve AnalysisLevel

.NET 5'ten beri Microsoft'un kod kalitesi analyzer'ları (Microsoft.CodeAnalysis.NetAnalyzers) SDK'nın içinde geliyor; net5.0 ve sonrasını hedefleyen her projede ek paket olmadan çalışıyorlar. Hangi kuralların hangi seviyede çalışacağını iki MSBuild property'si belirler. AnalysisLevel kural setinin hangi .NET sürümüne ait olduğunu, AnalysisMode ise o setten ne kadarının açılacağını söyler. Mode değerleri artan sırayla None, Default, Minimum, Recommended ve All. İkisi tek property'de birleşebilir: 10.0-recommended, latest-all gibi. Microsoft'un dokümanına göre ikisi birden yazılırsa bileşik AnalysisLevel değeri kazanır.

Varsayılanlar ilk yazıda gördüğümüz tabloyu açıklıyor. AnalysisLevel'ın varsayılanı latest, mode'un varsayılanı Default. .NET 10'da Default mod 29 kuralı warning ya da error olarak açıyor ve bunların neredeyse hepsi interop, stackalloc ya da file-based program gibi dar konularda. SDK'nın kendi config dosyalarına baktığımızda (sdk/<version>/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_recommended.globalconfig) Recommended modun 145 kuralı warning yaptığını görüyoruz; .NET 11'in aynı dosyasında bu sayı 151.

Tahminin cevabı burada. latest "elimdeki SDK'nın en yeni kural seti" demek. SDK 11'e geçince set de değişiyor:

$ dotnet --version
11.0.100
$ dotnet build Bookstore.slnx
src/Bookstore.Api/Catalog/Isbn.cs(14,13): error CA2028: Redundant call to 'Regex.IsMatch' before 'Regex.Match'. Use 'Regex.Match' and check 'Match.Success' instead.
    0 Warning(s)
    1 Error(s)

CA2028 .NET 11 ile gelen yeni bir kural: regex'i bir kez IsMatch ile, sonra bir kez daha Match ile çalıştırmak gereksiz; tek bir Match çağırıp Success'e bakmak yeterli. Kural haklı, ama bu PR'ın konusu değil. Çözüm seviyeyi sabitlemek:

<AnalysisLevel>10.0-recommended</AnalysisLevel>
$ dotnet build Bookstore.slnx
    0 Warning(s)
    0 Error(s)

Aynı SDK 11, aynı kod, sıfır hata. Dokümana göre sabitlenmiş seviyede SDK'yı yükselttiğinde mevcut kuralların bug fix'lerini alırsın ama yeni kural açılmaz, var olan kapanmaz. SDK yükseltmesi bir derleyici ve runtime değişikliği, kural setini yükseltmek ise bir kod kalitesi kararı; ikisini ayrı PR'larda yapmak hem review'u hem geri almayı kolaylaştırır. Sabitlemenin bedeli de var: latest'in kendiliğinden getirdiği iyi kuralları kaçırırsın. Bu yüzden takvime "her major SDK'dan sonra seviyeyi yükselt" diye bir iş koymak gerekir; altıncı yazıda .NET 11'in getirdiği kurallara tek tek bakacağız.

Kategori bazında: güvenlik kuralları

Her property'nin kategori bazında bir karşılığı var: AnalysisLevelSecurity, AnalysisLevelPerformance, AnalysisModeGlobalization gibi. Bookstore genel olarak Recommended ile yetinip güvenlik kategorisinde bütün kuralları açmak istiyor. Önce akla gelen yolu denedik, -p:AnalysisModeSecurity=All: hiçbir yeni uyarı çıkmadı. Sebep yukarıdaki öncelik kuralı. AnalysisLevel bileşik bir değer olduğunda kategori seviyesi de varsayılan olarak ondan türetiliyor ve mode'u eziyor. Doğru property AnalysisLevelSecurity:

$ dotnet build src/Bookstore.Api -p:AnalysisLevelSecurity=10.0-all
src/Bookstore.Api/Catalog/CouponCode.cs(13,32): warning CA5394: Random is an insecure random number generator. Use cryptographically secure random number generators when randomness is required for security.
src/Bookstore.Api/Reports/ReportsModule.cs(24,9): warning CA2100: Review if the query string passed to 'string DbCommand.CommandText' in 'GetRevenue', accepts any user input

Bu iki bulguyu aşağıda ele alacağız. Roslyn'in güvenlik kategorisi tek bir metodun içine bakan, kalıp tabanlı kurallardan oluşur; kullanıcı girdisinin method'lar arasında tehlikeli bir çağrıya kadar gittiği yolu izlemez. O iş SAST (Static Application Security Testing; kaynak kodu çalıştırmadan tarayan güvenlik testi) araçlarının; DevSecOps serisinin "SAST: CodeQL, Semgrep ve .NET Security Analyzer'ları ile Kaynak Kodda Güvenlik Açığı Aramak" yazısında aynı kuralların CodeQL ve Semgrep'le karşılaştırmasını yaptık.

.globalconfig mı, .editorconfig mi?

Kural severity'si iki tür dosyada yazılabilir ve ikisinin amacı farklı. .editorconfig dosya ve klasör bazlıdır: [*.cs], [tests/**/*.cs] gibi section başlıkları kuralın hangi dosyalara uygulanacağını seçer ve daha derindeki dosya kazanır. Global AnalyzerConfig dosyası ise section başlığı almaz, is_global = true satırıyla başlar ve projenin bütün kaynak dosyalarına uygulanır. Adı tam olarak .globalconfig olursa bulunduğu klasördeki ve alt klasörlerdeki bütün C# projelerine kendiliğinden eklenir; başka bir adla yazarsan GlobalAnalyzerConfigFiles item'ıyla eklemen gerekir.

Çakışma kuralları Microsoft Learn'de net yazıyor: aynı key hem .editorconfig'te hem global dosyada varsa .editorconfig kazanır. İki global dosya çakışırsa global_level değeri yüksek olan kazanır; .globalconfig adlı dosyalar için varsayılan 100. SDK'nın AnalysisLevel için eklediği config dosyaları da birer global config, ama global_level = -100 ile geliyor; bu yüzden bizim .globalconfig'imizdeki her satır onları ezer. Bookstore'un ayrımı:

  • .globalconfig: proje geneli kural kararları ve gerekçeleri. Code review'da tek dosyada okunur.
  • .editorconfig: yalnız yol bazlı istisnalar. Style kuralları da bu dosyada durur ama onlar Coding Conventions serisinin "C# Convention'larını .editorconfig ile Kodlamak" yazısının konusu; iki seri aynı dosyaya farklı açılardan bakıyor.

latest-recommended'a geçtiğimizde çıkan hatalardan ikisi yol bazlıydı: Entity Framework Core (EF Core; .NET'in veritabanı erişim kütüphanesi) migration'larında CA1861 ve test method adlarındaki alt çizgiler için CA1707. İkisini de kodu değiştirmeden, yalnız ilgili klasörde ele alıyoruz:

root = true

# Code style kuralları burada değil: Coding Conventions serisi.
# Bu dosyada yalnız yol bazlı analiz ayarları var; proje geneli kurallar .globalconfig'te.

[src/Bookstore.Api/Data/Migrations/*.cs]
generated_code = true

[tests/**/*.cs]
# Test adlarında alt çizgi Bookstore'un convention'ı: Placing_an_order_reserves_stock
dotnet_diagnostic.CA1707.severity = none

generated_code = true, migration dosyalarını dotnet ef'in ürettiği kod olarak işaretler ve analyzer'lar bu dosyaları atlar. Bunu yalnız gerçekten üretilen ve elle düzenlenmeyen dosyalar için kullan; bütün Data klasörünü işaretlemek gerçek kodu da analizden çıkarır.

Bookstore İçin Seçilmiş Kurallar

Recommended set iyi bir başlangıç ama ekip için asıl iş, hangi kuralın build'i kıracağına karar vermek. Bookstore'un ölçütü ilk yazıdaki gibi: bu kural bizim kodumuzda gerçek bir hatayı yakaladı mı ya da yakalar mı? Sonuç .globalconfig:

is_global = true

# Bookstore kod analizi kuralları. Kural seti Directory.Build.props'tan geliyor
# (AnalysisLevel = 10.0-recommended); bu dosya yalnız kararlarımızı kaydeder.

# Gerçekten production'a kaçırdığımız hatalar: build'i kırarlar.
# CA2016: CancellationToken'ı ilet
dotnet_diagnostic.CA2016.severity = error
# CA1305: kültüre bağlı parse ve format
dotnet_diagnostic.CA1305.severity = error
# CA1309: kod, slug ve ISBN için ordinal karşılaştırma
dotnet_diagnostic.CA1309.severity = error
# CA2100: girdiden üretilen SQL metni
dotnet_diagnostic.CA2100.severity = error
# CA5394: kod ve token için Random
dotnet_diagnostic.CA5394.severity = error
# EF1002 / EF1003: FromSqlRaw/ExecuteSqlRaw'da interpolated ya da birleştirilmiş string
dotnet_diagnostic.EF1002.severity = error
dotnet_diagnostic.EF1003.severity = error

# Warning olarak kalır: IDE'de ve PR'da görünür, build'i kırmaz.
# CA1848: LoggerMessage source generator
dotnet_diagnostic.CA1848.severity = warning

# Bilerek kapalı.
# CA2007: ASP.NET Core'da SynchronizationContext yok
dotnet_diagnostic.CA2007.severity = none

CA2016 ve CA1305'i ilk yazıda gördük. CA1309'un Bookstore'daki karşılığı kampanya kuponu. Bu kod Recommended setle derlendiğinde build'i kırıyor:

// Kampanya kuponları: "WINTER25" gibi elle verilenler ya da "K7QX2MPA9R" gibi üretilenler.
public static class CouponCode
{
    private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

    public static string NewCode()
    {
        char[] code = new char[10];
        for (int i = 0; i < code.Length; i++)
        {
            code[i] = Alphabet[Random.Shared.Next(Alphabet.Length)];
        }
        return new string(code);
    }

    public static bool Matches(string input, string code) =>
        string.Equals(input.Trim(), code, StringComparison.CurrentCultureIgnoreCase);
}
src/Bookstore.Api/Catalog/CouponCode.cs(19,43): error CA1309: Use ordinal string comparison

"Büyük küçük harf duyarsız karşılaştırma zaten yapılmış, neden ordinal?" sorusunun cevabı Türkçe'de. Aynı karşılaştırmayı iki kültürde çalıştırdık:

en-US: CurrentCultureIgnoreCase=True, OrdinalIgnoreCase=True
tr-TR: CurrentCultureIgnoreCase=False, OrdinalIgnoreCase=True

Türkçe'de küçük "i"nin büyüğü "İ" olduğu için tr-TR kültürüyle çalışan bir sunucuda "winter25" yazan müşterinin kuponu "WINTER25" ile eşleşmiyor. Kupon kodu dilsel bir metin değil, bir tanımlayıcı; tanımlayıcılar ordinal karşılaştırılır. Aynı dosyadaki Random.Shared ise CA5394: Random tahmin edilebilir bir üreteç ve tahmin edilebilen kupon kodu, ödenmemiş indirim demek. Düzeltilmiş hâli:

public static string NewCode() => RandomNumberGenerator.GetString(Alphabet, 10);

public static bool Matches(string input, string code) =>
    string.Equals(input.Trim(), code, StringComparison.OrdinalIgnoreCase);

CA2100 ve CA5394 Recommended setin içinde değil, bu yüzden .globalconfig'te açıkça error yapıyoruz; AnalysisLevelSecurity güvenlik kategorisinin geri kalanını warning olarak açık tutuyor. CA2100'ün yakaladığı kod back office'in kategori bazlı gelir raporu. EF Core'un dışına çıkıp ADO.NET (.NET'in düşük seviyeli veritabanı erişim API'si) ile yazılmış:

command.CommandText = """
    SELECT COALESCE(SUM(i.LineTotal), 0)
    FROM ordering.OrderItems i
    JOIN catalog.Books b ON b.Id = i.BookId
    JOIN catalog.Categories c ON c.Id = b.CategoryId
    WHERE c.Slug = '
    """ + category + "'";

Kuralın mesajı "accepts any user input" diye soruyor; cevabı test edelim. Bir siparişi olan veritabanında, kuralı geçici olarak NoWarn ile kapatıp çalıştırdığımızda:

$ curl "http://localhost:5080/api/admin/reports/revenue?category=classics"
{"category":"classics","revenue":499.80}  HTTP 200
$ curl "http://localhost:5080/api/admin/reports/revenue?category=nothing'%20OR%20'1'='1"
{"category":"nothing' OR '1'='1","revenue":988.80}  HTTP 200

Var olmayan bir kategori bütün cironun toplamını döndürdü. Parametreli hâli:

command.CommandText = """
    SELECT COALESCE(SUM(i.LineTotal), 0)
    FROM ordering.OrderItems i
    JOIN catalog.Books b ON b.Id = i.BookId
    JOIN catalog.Categories c ON c.Id = b.CategoryId
    WHERE c.Slug = @category
    """;
DbParameter parameter = command.CreateParameter();
parameter.ParameterName = "@category";
parameter.Value = category;
command.Parameters.Add(parameter);

decimal revenue = Convert.ToDecimal(await command.ExecuteScalarAsync(ct), CultureInfo.InvariantCulture);
{"category":"classics","revenue":499.80}  HTTP 200
{"category":"nothing' OR '1'='1","revenue":0.00}  HTTP 200

Son satırdaki CultureInfo.InvariantCulture da tesadüf değil: Convert.ToDecimal(object) için CA1305 aynı build'de hata vermişti. EF Core içinde kalınabilecek bir sorguysa db.Database.SqlQuery<decimal>($"...") interpolated string'i kendiliğinden parametreye çevirir; ADO.NET'e inmek gerektiğinde kural seni parametreye zorlar.

CA1848'i warning bıraktık. İlk yazıdaki log satırını source-generated bir method'a çevirmek kolay:

[LoggerMessage(Level = LogLevel.Information, Message = "Price import finished: {Updated} updated, {Unknown} unknown")]
private static partial void LogImportFinished(ILogger logger, int updated, int unknown);

Ama her log satırı için bunu zorunlu kılmak, hot path'te olmayan yüzlerce satırda ekibe değerinden fazla iş çıkarır. Warning olarak PR'da görünmesi yeterli. CA2007'yi ise açıkça kapattık; Recommended set onu zaten açmıyor, ama satırın dosyada durması "bunu biliyoruz ve bilerek kapalı" demek. Library yazan bir ekip için karar tam tersi olurdu.

TreatWarningsAsErrors mı, Hedefli Hata mı?

Bookstore'un başlangıç ayarı TreatWarningsAsErrors idi. İki seçenek ve tradeoff'ları:

Birinci seçenek, her warning'i error yapmak. Avantajı basitlik: build ya temizdir ya kırıktır, warning birikmez. Dezavantajı, kontrol etmediğin her şeyin de build'i kırması: yeni bir SDK'nın getirdiği kural, bir paketin [Obsolete] işaretlediği API, ya da DevSecOps serisinde anlattığımız NuGetAudit'in gece yayımlanan bir güvenlik bildirimi için ürettiği NU1903. Kodda değişiklik yokken CI kırılır ve ekip bunun için genellikle en kaba çözüme, NoWarn'a uzanır.

İkinci seçenek, hangi diagnostic'in build'i kıracağını tek tek seçmek. Bookstore bunu iki yerde yapıyor: CA (code analysis; SDK'nın kod kalitesi analyzer'ları) kuralları için .globalconfig'te severity = error, derleyicinin nullable uyarıları için MSBuild'in WarningsAsErrors'ı:

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Kural seti .NET 10 sürümüne sabit; yükseltme ayrı bir PR'da yapılır. -->
    <AnalysisLevel>10.0-recommended</AnalysisLevel>
    <AnalysisLevelSecurity>10.0-all</AnalysisLevelSecurity>

    <!-- Build'i yalnız bunlar kırar; CA kurallarının severity'si .globalconfig'te. -->
    <WarningsAsErrors>$(WarningsAsErrors);nullable</WarningsAsErrors>

    <!-- CI'da her proje için SARIF 2.1 dosyası yazılır. -->
    <ErrorLog Condition="'$(CI)' == 'true'">$(MSBuildThisFileDirectory)artifacts/sarif/$(MSBuildProjectName).sarif,version=2.1</ErrorLog>
  </PropertyGroup>
</Project>

nullable özel bir değer: derleyicinin bütün nullable uyarılarını (CS8602 gibi) tek kelimeyle kapsar. İlk yazıdaki book.Price satırı bu ayarla yine error CS8602 veriyor. Severity'yi .globalconfig'te error yapmanın WarningsAsErrors'a göre bir farkı var: kural IDE'de de kırmızı görünür, geliştirici build'i beklemeden görür.

Bu seçeneğin bedeli, warning'lerin birikebilmesi. Bookstore bunu SARIF (Static Analysis Results Interchange Format; analiz sonuçları için standart JSON formatı) çıktısı ve PR annotation'ıyla görünür tutuyor ve warning sayısını beşinci yazıdaki dashboard'da izliyor. Ekibin disiplini zayıfsa birinci seçenek daha güvenli; legacy bir codebase'te ise ikinci seçenek kademeli geçişin tek gerçekçi yolu. Bir ara yol: lokal build'de hedefli hatalar, CI'da -warnaserror. Dokümana göre CodeAnalysisTreatWarningsAsErrors=false ile bu bayrağın CA kurallarını etkilemesini de engelleyebilirsin.

Bastırma Türleri ve Gerekçe Zorunluluğu

Ana sayfadaki "öne çıkan kitaplar" için dört rastgele kitap seçen endpoint, yeni güvenlik ayarıyla build'i kırdı:

src/Bookstore.Api/Catalog/CatalogModule.cs(84,9): error CA5394: Random is an insecure random number generator. Use cryptographically secure random number generators when randomness is required for security.

Bu bir false positive: hangi kitabın vitrinde görüneceği bir sır değil, kimseye yetki vermiyor. Bastırmanın beş yolu var ve kapsamları çok farklı:

YolKapsamGerekçe yazılabilir mi?
#pragma warning disable CA5394Satırlar arasıYalnız yorum olarak
[SuppressMessage]Member, typeEvet, Justification
GlobalSuppressions.cs (assembly seviyesi SuppressMessage)Hedeflenen member ya da namespaceEvet
.editorconfig / .globalconfig'te severity = noneKlasör ya da projeYorum olarak
NoWarnProjeHayır

Bookstore'un kuralı: tek bir yerdeki false positive için SuppressMessage ve dolu bir Justification; bir kural bütün projede anlamsızsa config'te none ve yanında yorum. #pragma ve NoWarn code review'da reddediliyor.

// Ana sayfa için dört rastgele kitap; her istekte farklı bir seçim.
[SuppressMessage("Security", "CA5394:Do not use insecure randomness",
    Justification = "Picks books to show on the home page; the result is public and grants nothing.")]
private static async Task<Ok<BookSummary[]>> GetFeatured(BookstoreContext db, CancellationToken ct)
{
    BookSummary[] books = await db.Books.AsNoTracking()
        .Select(b => new BookSummary(b.Isbn, b.Title, b.Author.Name, b.Category.Name, b.Price, b.PublishedYear))
        .ToArrayAsync(ct);
    Random.Shared.Shuffle(books);
    return TypedResults.Ok(books[..Math.Min(4, books.Length)]);
}

Justification'ın boş bırakılmasını SDK engellemiyor. Bunu araçla zorlamak istersen StyleCop.Analyzers paketindeki SA1404 kuralı tam bunu yapar. Paket yüzlerce style kuralıyla geldiği için diğer kategorileri kapatıp yalnız bu kuralı açtık (dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = none gibi yedi satır ve dotnet_diagnostic.SA1404.severity = error), sonra gerekçeyi boşalttık:

src/Bookstore.Api/Catalog/CatalogModule.cs(81,9): error SA1404: Code analysis suppression should have justification
CSC : warning SA0001: XML comment analysis is disabled due to project configuration

İkinci satır, paketin XML dokümantasyon üretimi kapalı projelerde verdiği bilgi uyarısı; o da config'te kapatılabilir. Üçüncü parti analyzer'ları bir sonraki yazıda ayrıntılı ele alacağız; Bookstore şimdilik bu kuralı review checklist'inde tutuyor.

SARIF ve PR Annotation

Build log'u kimse okumaz; uyarının PR'daki satırın yanında görünmesi gerekir. Roslyn bunun için SARIF üretebilir. Yukarıdaki Directory.Build.props'taki ErrorLog satırı, CI environment variable'ı true olduğunda her proje için ayrı bir dosya yazıyor; GitHub Actions bu değişkeni kendisi set eder. İlk denemede build şu hatayla kırıldı:

CSC : error CS0016: Could not write to output file '.../artifacts/sarif/Bookstore.Api.sarif' -- 'Could not find a part of the path '.../artifacts/sarif/Bookstore.Api.sarif'.'

Derleyici klasörü oluşturmuyor; workflow'da önceden mkdir gerekiyor. Dosyadaki bir sonuç (yalnız yolu kısalttık):

{
  "ruleId": "CA1849",
  "ruleIndex": 168,
  "level": "warning",
  "message": {
    "text": "'WebApplication.Run(string?)' synchronously blocks. Await 'WebApplication.RunAsync(string?)' instead."
  },
  "locations": [
    {
      "physicalLocation": {
        "artifactLocation": {
          "uri": "file:///.../src/Bookstore.Api/Program.cs"
        },
        "region": {
          "startLine": 48,
          "startColumn": 1,
          "endLine": 48,
          "endColumn": 10
        }
      }
    }
  ]
}

Workflow, sonuçları GitHub code scanning'e yükleyen upload-sarif action'ını kullanıyor. Action'ın checkout_path girdisi SARIF'teki mutlak yolları repo'ya göre göreli hâle getiriyor, bu yüzden file:/// yolları sorun olmuyor:

name: dotnet-analysis

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          global-json-file: global.json
      # Derleyici SARIF klasörünü oluşturmuyor (CS0016)
      - run: mkdir -p artifacts/sarif
      - run: dotnet build Bookstore.slnx
      - name: Upload analyzer results
        if: ${{ !cancelled() }}
        uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: artifacts/sarif
          category: dotnet-analyzers

if: ${{ !cancelled() }} önemli: build kırıldığında da sonuçlar yüklenmeli, yoksa en çok ihtiyaç duyduğun anda annotation göremezsin. Public repo'larda code scanning ücretsiz; private repo'larda GitHub'ın Code Security ürünü gerekir, DevSecOps serisinde bu lisans ayrımını anlattık. Workflow'u GitHub Actions serisinin ".NET CI" yazısındaki pipeline'a ayrı bir job olarak da ekleyebilirsin.

.ruleset'ten .globalconfig'e

.NET Framework döneminden kalan projelerde kurallar genellikle bir .ruleset XML dosyasında ve CodeAnalysisRuleSet property'siyle bağlı. SDK 10'da hâlâ çalışıyor; denedik, ruleset'te Action="Error" verilen CA1849 build'i kırdı. Ama Microsoft ruleset'i deprecated sayıyor ve bir ruleset ile bir config dosyası aynı kurala farklı severity verirse hangisinin kazanacağı tanımsız. Taşıma mekanik:

<RuleSet Name="Bookstore rules" Description="Legacy rule set" ToolsVersion="17.0">
  <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.NetAnalyzers">
    <Rule Id="CA1849" Action="Error" />
    <Rule Id="CA2007" Action="None" />
    <Rule Id="CA1848" Action="Info" />
  </Rules>
</RuleSet>
is_global = true
dotnet_diagnostic.CA1849.severity = error
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1848.severity = suggestion

Action değerleri sırayla Error → error, Warning → warning, Info → suggestion, Hidden → silent, None → none olarak karşılanır. Visual Studio'nun ruleset'i .editorconfig'e çeviren bir komutu var; çıktıyı yine de satır satır gözden geçir, çünkü eski ruleset'ler çoğu zaman artık var olmayan FxCop (.NET Framework döneminin ikili kod analizi aracı) kurallarını da içerir. Taşıma bittiğinde CodeAnalysisRuleSet satırını sil; ikisini birlikte tutmak, yukarıdaki tanımsız önceliği kalıcı yapar.

Üretimde Dikkat

Seviye yükseltmesini ölçerek yap. 10.0-recommended'dan 11.0-recommended'a geçmeden önce aynı SDK'yla iki build al, yeni çıkan uyarıları kural ID'sine göre say ve ayrı bir PR'da ya düzelt ya da gerekçesiyle kapat. Tahmin bölümündeki tek CA2028 bu PR'ın konusu.

IDE ile CI aynı kuralları görsün. Bütün ayarlar Directory.Build.props, .globalconfig ve .editorconfig'te, yani repo'da. Visual Studio'nun "Analyze and Code Cleanup" ayarları gibi makineye özel ayarlar CI'da yok; IDE'de kırmızı görünmeyen bir şeyin CI'da kırılması, ya da tersi, ekibin bu dosyaları kullanmadığı anlamına gelir.

SARIF'in boyutunu izle. All modda Bookstore.Api tek başına 90 sonuç üretti. GitHub'ın SARIF yükleme limitleri var; binlerce sonuç üreten legacy bir projede önce severity'leri düzenle, sonra yükle.

Sık Düşülen Tuzaklar

AnalysisModeSecurity'nin çalıştığını sanmak. AnalysisLevel bileşik bir değerse kategori mode'u etkisiz kalıyor; bunu deneyerek gördük. Kategori için AnalysisLevel<Category>'i bileşik değerle kullan.

generated_code'u geniş bir glob'a vermek. [src/**/Data/**.cs] gibi bir section, elle yazılmış BookstoreContext'i de "üretilmiş" sayar ve analizden çıkarır.

Microsoft.CodeAnalysis.NetAnalyzers paketini SDK'nın yanına eklemek. Paket, analyzer'ları SDK'dan ayırıp kendi sürümüne bağlamak için var; eklediğinde SDK'daki analyzer'lar kapanır. Paketin sürümü SDK'nınkinden eskiyse build uyarı verir. Sebebin yoksa sürümü AnalysisLevel ile sabitle, paketi ekleme.

SARIF klasörünü oluşturmayı unutmak. CS0016 analyzer bulgusu değil, derleyicinin dosya yazamaması; build kırılır ve annotation da yüklenmez.

Küçük Bir Deneme

★☆☆ Kendi projende SDK'nın analysislevel_10_recommended.globalconfig dosyasını aç ve warning olan kuralların içinden senin kodunda en çok ihlal edilen üçünü bul. Her biri için "error, warning ya da none" kararını ve gerekçesini bir .globalconfig satırı olarak yaz.

★★☆ CA2028'i tetikleyen Isbn.Normalize'ı tek bir Match çağrısı ve match.Success ile yeniden yaz, sonra .NET 11 SDK'sıyla latest-recommended build'inin temiz geçtiğini doğrula. Regex'i [GeneratedRegex] property'sine çevirdiğinde kural hâlâ tetikleniyor mu?

★★★ Ekibindeki bir legacy projenin .ruleset dosyasını .globalconfig'e taşı. Taşımadan önce ve sonra -p:AnalysisLevel=latest-all ile SARIF üret ve iki dosyadaki sonuçları kural ID'sine göre karşılaştıran küçük bir script yaz. Fark çıkıyorsa sebebi ne?

SDK'nın analyzer'larını artık seviyesiyle, severity'siyle ve CI çıktısıyla yönetiyoruz. Ama Bookstore'a özgü kurallar, örneğin "domain kodunda DateTime.UtcNow değil TimeProvider", SDK'da yok. Sıradaki yazıda üçüncü parti analyzer paketlerini, build süresine etkilerini ve kendi Roslyn analyzer'ımızı code fix'i ve testleriyle yazmayı göreceğiz.