Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Application Security

Static Code Analysis for C#: Roslyn & CI/CD

Set up C# static code analysis step-by-step: enable Roslyn security rules, make CA2100/CA3001 build errors, and integrate GitHub Actions & Azure DevOps.

Offensive360 Security Research Team — min read
static code analysis C# C# static code analysis .NET static code analysis Roslyn analyzers Visual Studio static analysis GitHub Actions .NET security Azure DevOps SAST C# security tools SAST .NET security RunAnalyzersDuringBuild AnalysisLevel

Static code analysis for C# examines your source code — without running it — to find security vulnerabilities, performance issues, and coding standard violations. In .NET, static analysis runs at two levels: the Roslyn analyzer layer built into the compiler (fast, free, runs in Visual Studio), and dedicated SAST tools (deep taint analysis across class boundaries, second-order injection detection, full CI/CD integration).

This guide covers how to set up both layers: enabling Roslyn security analyzers in Visual Studio and your .csproj, running analysis in GitHub Actions and Azure DevOps, and understanding when you need a dedicated SAST tool for C# beyond what Roslyn provides.


How .NET Static Code Analysis Works

The .NET SDK includes the Roslyn compiler, which supports analyzer plugins — rules that run during compilation and produce warnings alongside normal build diagnostics. These analyzers see the abstract syntax tree (AST) of your code as it’s being compiled, which lets them catch patterns like:

  • A SqlCommand initialized with a string-concatenated query (CA2100)
  • A call to MD5.Create() for cryptographic purposes (CA5351)
  • User input written to HttpResponse.Write() without encoding (CA3001)
  • System.Random used for session tokens or security codes (CA5394)

The key limitation: Roslyn analyzers perform intra-method analysis only. They detect a vulnerability when the dangerous pattern appears within a single method. They cannot trace data flow across multiple method calls, class boundaries, or files — so a SQL injection chain where user input travels through a service layer and a repository class before reaching a SqlCommand is invisible to Roslyn.

For that level of coverage, you need a SAST tool with interprocedural taint analysis. More on that below.


Step 1: Enable Static Analysis in Your .csproj

The fastest way to enable .NET static code analysis is to configure AnalysisLevel in your project file:

<!-- In your .csproj -->
<PropertyGroup>
  <!-- Enable all recommended rules at the latest SDK level -->
  <AnalysisLevel>latest-recommended</AnalysisLevel>

  <!-- Run analyzers during dotnet build (not just in the IDE) -->
  <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
</PropertyGroup>

latest-recommended enables the broadest set of quality and security rules appropriate for your .NET version. Alternative values:

ValueWhat It Does
latestAll rules at the latest SDK level
latest-recommendedRecommended rules (fewer false positives)
8.0 or 9.0Specific .NET version rules
noneAnalyzers disabled

Treat Specific Security Rules as Errors

To block your build (and CI pipeline) when high-risk security violations are introduced, promote specific rules to errors:

<PropertyGroup>
  <AnalysisLevel>latest-recommended</AnalysisLevel>
  <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>

  <!-- These security violations fail the build -->
  <WarningsAsErrors>
    CA2100;  <!-- SQL injection (ADO.NET) -->
    CA3001;  <!-- XSS via HttpResponse.Write -->
    CA3002;  <!-- LDAP injection -->
    CA3003;  <!-- File path injection / path traversal -->
    CA3006;  <!-- OS command injection -->
    CA3007;  <!-- Open redirect -->
    CA5350;  <!-- Weak crypto: TripleDES -->
    CA5351;  <!-- Broken crypto: MD5, DES, RC2 -->
    CA5360;  <!-- Dangerous BinaryFormatter use -->
    CA5394   <!-- Insecure randomness (System.Random) -->
  </WarningsAsErrors>
</PropertyGroup>

With this configuration, introducing a SqlCommand with string concatenation or a call to MD5.Create() will fail the build — in the IDE, in dotnet build, and in your CI pipeline.


Step 2: Static Analysis in Visual Studio

Visual Studio shows Roslyn analyzer findings inline as you type — no separate tool run required. The findings appear as squiggly underlines in the editor:

  • Red underline — error (if the rule is configured as an error)
  • Green underline — warning
  • Grey underline — suggestion / informational

Viewing All Analyzer Findings

In Visual Studio:

  1. Error List (View → Error List) — shows all current analyzer findings
  2. Analyze → Run Code Analysis on Solution — runs all configured analyzers on the full solution
  3. Analyze → Run Code Analysis on [Project] — analyzes a single project

Suppressing False Positives

When an analyzer finding is a known false positive, suppress it with an inline attribute:

// Suppress a specific rule for a method (add justification comment)
[System.Diagnostics.CodeAnalysis.SuppressMessage(
    "Security",
    "CA2100:Review SQL queries for security vulnerabilities",
    Justification = "Query uses a known-safe stored procedure name from a compile-time constant")]
public IEnumerable<User> GetUsersByProcedure()
{
    // Safe: the SQL string is a compile-time constant, not user input
    const string sql = "EXEC dbo.GetAllUsers";
    return _db.Query<User>(sql);
}

Suppression should be used sparingly — every suppression is a gap in your security coverage.

Installing Additional Security Analyzers in Visual Studio

Add security-focused NuGet analyzers for deeper coverage:

<!-- SecurityCodeScan — additional security-focused rules for ASP.NET -->
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

<!-- SonarAnalyzer.CSharp — SonarSource rules as a Roslyn analyzer -->
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

After adding these packages, the new rules appear automatically in Visual Studio and in dotnet build output.


Step 3: Static Code Analysis in GitHub Actions

To run C# static analysis in GitHub Actions with security rules enforced:

name: .NET Security Analysis

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  static-analysis:
    name: C# Static Code Analysis
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.x'

      - name: Restore dependencies
        run: dotnet restore

      - name: Run static code analysis
        run: |
          dotnet build \
            --configuration Release \
            --no-restore \
            /p:RunAnalyzersDuringBuild=true \
            /p:AnalysisLevel=latest-recommended \
            /warnaserror:CA2100,CA3001,CA3002,CA3003,CA3006,CA3007,CA5350,CA5351,CA5360,CA5394
        # Build fails (and PR is blocked) if any of these security rules are violated

Uploading SARIF Results to GitHub Security Tab

To have analyzer findings appear in the GitHub Security → Code Scanning tab:

      - name: Run analysis and generate SARIF
        run: |
          dotnet build \
            --configuration Release \
            /p:RunAnalyzersDuringBuild=true \
            /p:AnalysisLevel=latest-recommended \
            /p:ErrorLog=results.sarif%3BSeverity%3Derror%2Cwarning
        continue-on-error: true  # Don't fail here — let the upload step run first

      - name: Upload SARIF results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
        if: always()  # Upload even if the build failed

This integrates Roslyn findings into GitHub’s native security dashboard — no third-party tooling required.


Step 4: Static Code Analysis in Azure DevOps

# azure-pipelines.yml
trigger:
  - main
  - develop

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'

steps:
  - task: UseDotNet@2
    displayName: 'Setup .NET 8'
    inputs:
      version: '8.x'

  - task: DotNetCoreCLI@2
    displayName: 'Restore'
    inputs:
      command: 'restore'
      projects: '**/*.csproj'

  - task: DotNetCoreCLI@2
    displayName: 'Build with Security Analysis'
    inputs:
      command: 'build'
      projects: '**/*.csproj'
      arguments: >
        --configuration $(buildConfiguration)
        --no-restore
        /p:RunAnalyzersDuringBuild=true
        /p:AnalysisLevel=latest-recommended
        /warnaserror:CA2100,CA3001,CA3002,CA3003,CA3006,CA5350,CA5351,CA5394
    # Build task fails if any of the specified rules are violated

Azure DevOps: Separate Security Scan Step

For a cleaner pipeline structure with a dedicated security scan phase:

stages:
  - stage: Build
    jobs:
      - job: BuildAndTest
        steps:
          - task: DotNetCoreCLI@2
            displayName: 'Restore and Build'
            inputs:
              command: 'build'
              arguments: '--configuration Release /p:RunAnalyzersDuringBuild=false'
              # Fast build without analyzers for normal checks

  - stage: SecurityScan
    dependsOn: Build
    jobs:
      - job: StaticAnalysis
        steps:
          - task: DotNetCoreCLI@2
            displayName: 'Security-Gated Build'
            inputs:
              command: 'build'
              arguments: >
                --configuration Release
                /p:RunAnalyzersDuringBuild=true
                /p:AnalysisLevel=latest-recommended
                /warnaserror:CA2100,CA3001,CA3006,CA5350,CA5351
            # This stage fails if security rules are violated

The dotnet ef and RunAnalyzersDuringBuild Conflict

A common issue when using dotnet ef (Entity Framework migrations) alongside Roslyn security analyzers: dotnet ef migrations add or dotnet ef database update fails because EF’s internal project build triggers your analyzers — and if any analyzer emits an error, the EF command aborts.

The fix: pass RunAnalyzersDuringBuild=false to the EF command via the -- argument separator:

# The -- separator forwards MSBuild properties to the EF tool's internal build
dotnet ef migrations add MyMigration -- /p:RunAnalyzersDuringBuild=false
dotnet ef database update -- /p:RunAnalyzersDuringBuild=false

This disables analyzers only for the EF design-time build — not for your main application build or CI security scan. See the complete dotnet ef RunAnalyzersDuringBuild guide for all command variants and the correct CI/CD pattern.

Critical: Never set RunAnalyzersDuringBuild=false globally in your .csproj — that disables all security analysis on every build.


Key C# Security Rules to Enable

These are the most important Roslyn security rules to configure as build errors for C# and ASP.NET projects:

SQL Injection (CWE-89)

Rule: CA2100 — fires when a SqlCommand, OleDbCommand, or OdbcCommand is initialized with a CommandText value built by string concatenation or interpolation.

// CA2100 violation — user input in SqlCommand
string username = Request.QueryString["username"];
string sql = "SELECT * FROM Users WHERE Username = '" + username + "'";
var cmd = new SqlCommand(sql, conn);  // CA2100 fires here

// SECURE — parameterized query
var cmd = new SqlCommand("SELECT * FROM Users WHERE Username = @u", conn);
cmd.Parameters.AddWithValue("@u", username);

Limitation: CA2100 only fires when the dangerous pattern appears within the same method. SQL injection chains spanning multiple methods (controller → service → repository) are not detected by Roslyn. A dedicated SAST tool is required for those cases.

XSS (CWE-79)

Rule: CA3001 — fires when user-controlled data flows into HttpResponse.Write() without encoding.

// CA3001 violation — unencoded user input in response
string searchTerm = Request.QueryString["q"];
Response.Write("<h1>Results for: " + searchTerm + "</h1>");  // CA3001

// SECURE — HTML encode first
Response.Write("<h1>Results for: " + HttpUtility.HtmlEncode(searchTerm) + "</h1>");

In Razor views, @Model.Property is automatically HTML-encoded. The dangerous pattern is @Html.Raw(Model.Property) — Roslyn cannot catch this through CA3001, but a dedicated SAST tool will flag it.

Command Injection (CWE-78)

Rule: CA3006 — fires when user-controlled data flows into Process.Start() arguments.

// CA3006 violation
string inputFile = Request.Form["filename"];
Process.Start("convert", inputFile + " output.pdf");  // CA3006

// SECURE — strict validation + ArgumentList (no shell parsing)
if (!Regex.IsMatch(inputFile, @"^[a-zA-Z0-9_\-]+\.pdf$"))
    return BadRequest();

var psi = new ProcessStartInfo { FileName = "convert" };
psi.ArgumentList.Add(inputFile);
psi.ArgumentList.Add("output.pdf");
Process.Start(psi);

Weak Cryptography (CWE-327 / CWE-326)

Rules: CA5350, CA5351 — fire on use of TripleDES, MD5, DES, or RC2.

// CA5351 violation — MD5 for password hashing
using var md5 = MD5.Create();  // CA5351
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));

// SECURE — use PBKDF2
using var kdf = new Rfc2898DeriveBytes(
    password,
    RandomNumberGenerator.GetBytes(16),
    iterations: 600_000,
    HashAlgorithmName.SHA512
);

Adding a Dedicated SAST Tool for C#

Roslyn analyzers are a strong first layer but they have fundamental limitations:

CapabilityRoslyn AnalyzersDedicated SAST (e.g., Offensive360)
Single-method vulnerability detection✅ Yes✅ Yes
Cross-method taint analysis❌ No✅ Yes
Cross-class / cross-file taint analysis❌ No✅ Yes
Second-order SQL injection detection❌ No✅ Yes
Entity Framework FromSqlRaw() injection❌ No✅ Yes
Stored XSS (data stored in DB, rendered later)❌ No✅ Yes
DAST (runtime testing)❌ No✅ Yes
SCA (NuGet vulnerability scanning)❌ No✅ Yes
SSRF, insecure deserialization chains❌ Limited✅ Yes

For enterprise .NET teams, the recommended approach is to use both:

  1. Roslyn analyzers — fast, in-IDE feedback for obvious patterns, zero additional cost
  2. Dedicated SAST (Offensive360) — interprocedural taint analysis in CI/CD, catches complex injection chains that Roslyn misses

Adding Offensive360 SAST to GitHub Actions

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Step 1: Roslyn security analysis (fast, in-build)
      - name: Build with Roslyn Security Analyzers
        run: |
          dotnet build \
            /p:RunAnalyzersDuringBuild=true \
            /p:AnalysisLevel=latest-recommended \
            /warnaserror:CA2100,CA3001,CA3006,CA5350,CA5351

      # Step 2: Deep SAST (interprocedural taint analysis)
      - name: Offensive360 SAST Scan
        env:
          O360_API_KEY: ${{ secrets.O360_API_KEY }}
        run: |
          curl -X POST https://api.offensive360.com/scan/code \
            -H "X-API-Key: $O360_API_KEY" \
            --data-binary @./src

This two-layer approach catches:

  • Layer 1 (Roslyn): obvious single-method patterns during the build — no additional latency
  • Layer 2 (Offensive360): complex multi-method injection chains, second-order vulnerabilities, and DAST findings — deeper but run asynchronously

Common C# Static Analysis Mistakes to Avoid

1. Disabling RunAnalyzersDuringBuild Globally

<!-- ❌ WRONG — silently disables all security analysis on every build -->
<PropertyGroup>
  <RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
</PropertyGroup>

This is sometimes added to fix dotnet ef migration failures. It is the wrong solution — it disables all security analyzers globally. Instead, pass the flag only to dotnet ef commands via the -- separator (see above).

2. Using TreatWarningsAsErrors Globally Without Review

<!-- ⚠️ Be careful — this treats ALL warnings as errors, including style warnings -->
<PropertyGroup>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

Setting TreatWarningsAsErrors globally will cause build failures on code style warnings (IDE0xxx), nullable reference warnings (CS8xxx), and deprecation notices — which may be too disruptive. The targeted approach using <WarningsAsErrors> with specific rule IDs (CA2100, CA3001, etc.) gives you security enforcement without over-blocking.

3. Suppressing Security Rules Without Justification

// ❌ WRONG — suppressing security rules without explanation
#pragma warning disable CA2100
var cmd = new SqlCommand(userInput, conn);
#pragma warning restore CA2100

Every suppression of a security rule should have a documented justification explaining exactly why the flagged code is safe. Suppressions without justification become invisible technical debt that future developers can’t evaluate.

4. Only Running Analyzers in the IDE, Not in CI

If RunAnalyzersDuringBuild=false is set but security rules show up in Visual Studio, you have a false sense of security — the CI build passes while the same rules would fail if enabled. Always verify that the rules you’re monitoring in the IDE are also enforced as errors in your CI pipeline.


Frequently Asked Questions

What is the difference between static code analysis and linting in C#?

A linter (like StyleCop or EditorConfig rules) checks code style, formatting, and naming conventions within a single file. Static code analysis (Roslyn security analyzers, dedicated SAST tools) checks for security vulnerabilities, logic errors, and dangerous patterns — potentially across multiple files and methods. Linting catches style inconsistencies; static analysis catches bugs and security flaws.

Does static code analysis in C# require source code?

Roslyn analyzers and all source-level SAST tools require source code. Veracode’s binary analysis is the exception — it works on compiled .NET assemblies. Source-level analysis is generally more accurate because it can trace data flows through framework internals that binary analysis cannot fully model.

Can Visual Studio’s built-in analysis find SQL injection?

Yes, for obvious single-method patterns — specifically when a SqlCommand is constructed with string concatenation in the same method as the untrusted input source. Rule CA2100 detects this. However, Visual Studio’s Roslyn analyzers cannot detect SQL injection across multiple method calls, which is the common pattern in real enterprise ASP.NET codebases. That requires interprocedural taint analysis from a dedicated SAST tool.

How do I run static code analysis on a .NET Framework project (4.x)?

Roslyn analyzers support .NET Framework 4.x when targeting the analyzer NuGet packages (Microsoft.CodeAnalysis.NetAnalyzers). Add the package reference to your .csproj and configure AnalysisLevel. Note that some newer rules only apply to .NET 5+ patterns. Dedicated SAST tools like Offensive360 support both .NET Framework 4.x and modern .NET.

latest-recommended is the better starting point — it enables a curated set of rules with fewer false positives. latest enables every rule at the latest SDK level, which may produce more noise before you tune the rules for your codebase. Start with latest-recommended, enforce the specific security rules as errors, then expand from there.


Getting Started

Immediate (5 minutes): Add <AnalysisLevel>latest-recommended</AnalysisLevel> and <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> to your .csproj. Run dotnet build and review the new warnings.

This week: Identify which security rules (CA2100, CA3001, CA5350, CA5351) have violations in your codebase. Fix the real violations, suppress genuine false positives with documented justification, and promote the rules to build errors.

This sprint: Add the security analysis step to your GitHub Actions or Azure DevOps pipeline. Block PR merges on Critical security findings.

This quarter: Evaluate whether Roslyn analyzers are catching the real injection vulnerabilities in your ASP.NET codebase — or whether the vulnerable code is spread across multiple classes in a way Roslyn can’t trace. A one-time SAST scan ($500) provides a quick baseline before committing to an annual subscription.


Offensive360 provides deep interprocedural taint analysis for C#, VB.NET, ASP.NET Core, and ASP.NET Framework — running alongside your Roslyn analyzers in CI/CD without replacing them. See the .NET SAST tool comparison or book a demo to see a live scan of your codebase.

Offensive360 Security Research Team

Application Security Research

Updated June 1, 2026

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.