Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf

# Source code
*.cs text eol=lf
*.csx text eol=lf
*.vb text eol=lf
*.fs text eol=lf
*.fsx text eol=lf

# Scripts
# PowerShell files use CRLF (Windows-style) line endings for convention compliance.
*.ps1 text eol=crlf


# Build and configuration files
*.xml text eol=lf
*.csproj text eol=lf
*.vbproj text eol=lf
*.fsproj text eol=lf
*.sln text eol=lf
*.slnx text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.ruleset text eol=lf
*.config text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf

# Documentation
*.md text eol=lf
*.txt text eol=lf

# SVG files (XML-based text)
*.svg text eol=lf

# Denote all files that are truly binary and should not be modified
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.dll binary
*.exe binary
*.nupkg binary
*.snupkg binary
184 changes: 184 additions & 0 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: "CodeQL Security Analysis"

on:
push:
branches: [ "main" ]
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read # Default to read-only; the analyze job overrides where required

jobs:
analyze:
name: "Security Scan (CodeQL)"
runs-on: windows-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Check for C# source code
id: check-csharp
shell: pwsh
run: |
Write-Host "=== CodeQL C# Source Code Detection ===" -ForegroundColor Cyan
Write-Host "Searching for C# source files using git index..." -ForegroundColor Cyan

# Use git ls-files to efficiently find C# files (respects .gitignore and doesn't traverse excluded dirs)
$csharpFiles = @()
try {
$gitOutput = git ls-files '*.cs' '*.csproj' 2>$null
if ($LASTEXITCODE -eq 0) {
$csharpFiles = @($gitOutput | Where-Object { $_ })
} else {
Write-Warning "git ls-files failed with exit code $LASTEXITCODE. Falling back to filesystem search for C# files."
}
} catch {
Write-Warning "Exception while running git ls-files: $($_.Exception.Message). Falling back to filesystem search for C# files."
}

if (-not $csharpFiles -or $csharpFiles.Count -eq 0) {
Write-Host "git ls-files did not return any C# files. Scanning the filesystem for *.cs and *.csproj files..." -ForegroundColor Cyan
$csharpFiles = @(Get-ChildItem -Path . -Recurse -Include *.cs,*.csproj -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
}
$csharpFileCount = $csharpFiles.Count

Write-Host "" -ForegroundColor Cyan
if ($csharpFileCount -eq 0) {
Write-Host "⚠️ No C# source code found." -ForegroundColor Yellow
Write-Host " This appears to be an empty repository or a repository without projects." -ForegroundColor Yellow
Write-Host " CodeQL analysis will be SKIPPED, but the job will complete successfully." -ForegroundColor Yellow
Write-Host " This ensures branch protection requirements are met." -ForegroundColor Yellow
echo "has-csharp=false" >> $env:GITHUB_OUTPUT
} else {
Write-Host "✅ Found $csharpFileCount C# file(s)." -ForegroundColor Green
Write-Host " CodeQL analysis will PROCEED." -ForegroundColor Green
echo "has-csharp=true" >> $env:GITHUB_OUTPUT
}
Write-Host "========================================" -ForegroundColor Cyan

- name: Initialize CodeQL
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- name: Setup .NET
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Build for CodeQL Analysis
id: build
if: steps.check-csharp.outputs.has-csharp == 'true'
shell: pwsh
run: |
Write-Host "Building solution for CodeQL analysis..."

# Find solution file (.sln or .slnx)
$solution = Get-ChildItem -Path . -Recurse -Depth 2 -Include "*.sln", "*.slnx" | Select-Object -First 1

if ($solution) {
Write-Host "Found solution: $($solution.FullName)"
dotnet restore $solution.FullName
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet restore failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
dotnet build $solution.FullName --configuration Release --no-restore
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet build failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
} else {
Write-Host "No solution file found, building all projects..."
dotnet restore
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet restore failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
dotnet build --configuration Release --no-restore
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet build failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
}

Write-Host "✅ Build completed for CodeQL analysis"

- name: Perform CodeQL Analysis
id: perform-codeql-analysis
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

- name: Complete Security Scan
if: always()
shell: pwsh
run: |
Write-Host "=== CodeQL Security Scan Complete ===" -ForegroundColor Cyan

# Check the outcome of previous steps
$checkCsharpOutcome = "${{ steps.check-csharp.outcome }}"
$hasCsharp = "${{ steps.check-csharp.outputs.has-csharp }}"
$buildOutcome = "${{ steps.build.outcome }}"
$codeqlOutcome = "${{ steps.perform-codeql-analysis.outcome }}"

# Determine overall status
$hasFailure = $false
$failureMessages = @()

# Check if check-csharp step failed
if ($checkCsharpOutcome -eq "failure") {
$hasFailure = $true
$failureMessages += "❌ C# source code detection failed"
}

# Check if build step failed (only relevant if C# code exists)
if ($hasCsharp -eq "true" -and $buildOutcome -eq "failure") {
$hasFailure = $true
$failureMessages += "❌ Build failed during CodeQL analysis"
}

# Check if CodeQL analysis step failed (only relevant if C# code exists)
if ($hasCsharp -eq "true" -and $codeqlOutcome -eq "failure") {
$hasFailure = $true
$failureMessages += "❌ CodeQL analysis failed"
}

# Display results based on actual step outcomes
if ($hasFailure) {
Write-Host "❌ Security scan completed with errors:" -ForegroundColor Red
foreach ($msg in $failureMessages) {
Write-Host " $msg" -ForegroundColor Red
}
exit 1
} elseif ($hasCsharp -eq "true") {
Write-Host "✅ CodeQL analysis completed successfully." -ForegroundColor Green
Write-Host " Results have been uploaded to GitHub Security." -ForegroundColor Green
} elseif ($hasCsharp -eq "false") {
Write-Host "✅ Security scan completed successfully (no C# code to analyze)." -ForegroundColor Green
Write-Host " This job ran successfully and reports a passing status to branch protection." -ForegroundColor Green
} else {
Write-Host "✅ Security scan job completed." -ForegroundColor Green
Write-Host " Job status reported to branch protection." -ForegroundColor Green
}
Write-Host "========================================" -ForegroundColor Cyan
10 changes: 10 additions & 0 deletions .globalconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
is_global = true

# Global analyzer configuration
global_level = 9999

# Roslynator configuration
roslynator_accessibility_modifiers = explicit
roslynator_enum_has_flag_style = method
roslynator_object_creation_type_style = implicit_when_type_is_obvious
roslynator_use_anonymous_function_or_method_group = method_group
81 changes: 81 additions & 0 deletions BannedSymbols.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# BannedSymbols.txt - Async-First Enforcement for Try-Pattern
# Format: <API Documentation ID>; <Reason/Alternative>
# T: = Type, M: = Method, P: = Property, F: = Field
# Task.Wait() - All overloads - Absolutely NOT allowed in async code
M:System.Threading.Tasks.Task.Wait; Use 'await' instead - this blocks the calling thread!
M:System.Threading.Tasks.Task.Wait(System.Int32); Use 'await' with CancellationToken timeout instead
M:System.Threading.Tasks.Task.Wait(System.TimeSpan); Use 'await' with CancellationToken timeout instead
M:System.Threading.Tasks.Task.Wait(System.Int32,System.Threading.CancellationToken); Use 'await' instead
Comment thread
Chris-Wolfgang marked this conversation as resolved.
M:System.Threading.Tasks.Task.Wait(System.Threading.CancellationToken); Use 'await' instead
# Task.WaitAll/WaitAny - Use async alternatives
M:System.Threading.Tasks.Task.WaitAll(System.Threading.Tasks.Task[]); Use 'await Task.WhenAll()' instead
M:System.Threading.Tasks.Task.WaitAll(System.Threading.Tasks.Task[],System.Int32); Use 'await Task.WhenAll()' with CancellationToken instead
M:System.Threading.Tasks.Task.WaitAll(System.Threading.Tasks.Task[],System.TimeSpan); Use 'await Task.WhenAll()' with CancellationToken instead
M:System.Threading.Tasks.Task.WaitAll(System.Threading.Tasks.Task[],System.Int32,System.Threading.CancellationToken); Use 'await Task.WhenAll()' instead
M:System.Threading.Tasks.Task.WaitAll(System.Threading.Tasks.Task[],System.Threading.CancellationToken); Use 'await Task.WhenAll()' instead
M:System.Threading.Tasks.Task.WaitAny(System.Threading.Tasks.Task[]); Use 'await Task.WhenAny()' instead
M:System.Threading.Tasks.Task.WaitAny(System.Threading.Tasks.Task[],System.Int32); Use 'await Task.WhenAny()' with CancellationToken instead
M:System.Threading.Tasks.Task.WaitAny(System.Threading.Tasks.Task[],System.TimeSpan); Use 'await Task.WhenAny()' with CancellationToken instead
M:System.Threading.Tasks.Task.WaitAny(System.Threading.Tasks.Task[],System.Int32,System.Threading.CancellationToken); Use 'await Task.WhenAny()' instead
M:System.Threading.Tasks.Task.WaitAny(System.Threading.Tasks.Task[],System.Threading.CancellationToken); Use 'await Task.WhenAny()' instead
# Task<T>.Result - Blocking property access
P:System.Threading.Tasks.Task`1.Result; Blocking! Use 'await' instead to get the result asynchronously
# GetAwaiter().GetResult() - Also blocking
M:System.Runtime.CompilerServices.TaskAwaiter.GetResult; Blocking! Use 'await' instead
M:System.Runtime.CompilerServices.TaskAwaiter`1.GetResult; Blocking! Use 'await' instead
# Thread.Sleep - Use Task.Delay for async delays
M:System.Threading.Thread.Sleep(System.Int32); Use 'await Task.Delay()' instead for async-friendly delays
M:System.Threading.Thread.Sleep(System.TimeSpan); Use 'await Task.Delay()' instead for async-friendly delays
# Obsolete/Deprecated Threading APIs
M:System.Threading.Thread.Suspend; Deprecated and dangerous
M:System.Threading.Thread.Resume; Deprecated and dangerous
T:System.ComponentModel.BackgroundWorker; Use async/await patterns instead of BackgroundWorker
# Synchronous File I/O - Use async versions
M:System.IO.File.ReadAllText(System.String); Use 'File.ReadAllTextAsync()' instead
M:System.IO.File.ReadAllText(System.String,System.Text.Encoding); Use 'File.ReadAllTextAsync()' instead
M:System.IO.File.ReadAllLines(System.String); Use 'File.ReadAllLinesAsync()' instead
M:System.IO.File.ReadAllLines(System.String,System.Text.Encoding); Use 'File.ReadAllLinesAsync()' instead
M:System.IO.File.ReadAllBytes(System.String); Use 'File.ReadAllBytesAsync()' instead
M:System.IO.File.WriteAllText(System.String,System.String); Use 'File.WriteAllTextAsync()' instead
M:System.IO.File.WriteAllText(System.String,System.String,System.Text.Encoding); Use 'File.WriteAllTextAsync()' instead
M:System.IO.File.WriteAllLines(System.String,System.Collections.Generic.IEnumerable{System.String}); Use 'File.WriteAllLinesAsync()' instead
M:System.IO.File.WriteAllLines(System.String,System.Collections.Generic.IEnumerable{System.String},System.Text.Encoding); Use 'File.WriteAllLinesAsync()' instead
M:System.IO.File.WriteAllLines(System.String,System.String[]); Use 'File.WriteAllLinesAsync()' instead
M:System.IO.File.WriteAllLines(System.String,System.String[],System.Text.Encoding); Use 'File.WriteAllLinesAsync()' instead
M:System.IO.File.WriteAllBytes(System.String,System.Byte[]); Use 'File.WriteAllBytesAsync()' instead
M:System.IO.File.AppendAllText(System.String,System.String); Use 'File.AppendAllTextAsync()' instead
M:System.IO.File.AppendAllText(System.String,System.String,System.Text.Encoding); Use 'File.AppendAllTextAsync()' instead
M:System.IO.File.AppendAllLines(System.String,System.Collections.Generic.IEnumerable{System.String}); Use 'File.AppendAllLinesAsync()' instead
M:System.IO.File.AppendAllLines(System.String,System.Collections.Generic.IEnumerable{System.String},System.Text.Encoding); Use 'File.AppendAllLinesAsync()' instead
# Synchronous Stream operations - Use async versions for file I/O
M:System.IO.Stream.Read(System.Byte[],System.Int32,System.Int32); Use 'ReadAsync()' instead
M:System.IO.Stream.Write(System.Byte[],System.Int32,System.Int32); Use 'WriteAsync()' instead
M:System.IO.Stream.CopyTo(System.IO.Stream); Use 'CopyToAsync()' instead
M:System.IO.Stream.CopyTo(System.IO.Stream,System.Int32); Use 'CopyToAsync()' instead
M:System.IO.Stream.Flush; Use 'FlushAsync()' instead
M:System.IO.FileStream.Read(System.Byte[],System.Int32,System.Int32); Use 'ReadAsync()' instead
M:System.IO.FileStream.Write(System.Byte[],System.Int32,System.Int32); Use 'WriteAsync()' instead
# Obsolete Network APIs - Use HttpClient
T:System.Net.WebClient; Obsolete - use HttpClient instead
T:System.Net.WebRequest; Obsolete - use HttpClient instead
T:System.Net.HttpWebRequest; Obsolete - use HttpClient instead
T:System.Net.HttpWebResponse; Obsolete - use HttpClient instead
M:System.Net.WebClient.DownloadString(System.String); Use 'HttpClient.GetStringAsync()' instead
M:System.Net.WebClient.DownloadData(System.String); Use 'HttpClient.GetByteArrayAsync()' instead
M:System.Net.WebClient.UploadString(System.String,System.String); Use 'HttpClient.PostAsync()' instead
# Obsolete/Insecure Serialization
T:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter; Insecure and deprecated - use System.Text.Json.JsonSerializer instead
T:System.Runtime.Serialization.Formatters.Soap.SoapFormatter; Insecure and deprecated - use System.Text.Json.JsonSerializer instead
# DateTime Anti-patterns - Prefer DateTimeOffset for timezone safety
P:System.DateTime.Now; Use 'DateTimeOffset.UtcNow' or 'DateTimeOffset.Now' for timezone-aware operations
# Synchronous Parallel operations - Use async alternatives
M:System.Threading.Tasks.Parallel.For(System.Int32,System.Int32,System.Action{System.Int32}); Synchronous - prefer async concurrency patterns (e.g., 'Task.WhenAll()', dataflow). On .NET 6+ targets, 'Parallel.ForEachAsync()' is also available.
M:System.Threading.Tasks.Parallel.For(System.Int32,System.Int32,System.Threading.Tasks.ParallelOptions,System.Action{System.Int32}); Synchronous - prefer async concurrency patterns (e.g., 'Task.WhenAll()', dataflow). On .NET 6+ targets, 'Parallel.ForEachAsync()' is also available.
M:System.Threading.Tasks.Parallel.ForEach``1(System.Collections.Generic.IEnumerable{``0},System.Action{``0}); Synchronous - prefer async concurrency patterns (e.g., 'Task.WhenAll()', dataflow). On .NET 6+ targets, 'Parallel.ForEachAsync()' is also available.
M:System.Threading.Tasks.Parallel.ForEach``1(System.Collections.Generic.IEnumerable{``0},System.Threading.Tasks.ParallelOptions,System.Action{``0}); Synchronous - prefer async concurrency patterns (e.g., 'Task.WhenAll()', dataflow). On .NET 6+ targets, 'Parallel.ForEachAsync()' is also available.
M:System.Threading.Tasks.Parallel.Invoke(System.Action[]); Synchronous - use 'Task.WhenAll()' with async delegates instead
# Console Blocking operations - Avoid in async code
M:System.Console.ReadLine; Blocking - avoid in async code paths
M:System.Console.Read; Blocking - avoid in async code paths
M:System.Console.ReadKey; Blocking - avoid in async code paths
M:System.Console.ReadKey(System.Boolean); Blocking - avoid in async code paths
59 changes: 59 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>

<!-- Enable .NET analyzers -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

<!-- Suppress warning about NetAnalyzers package since we're using SDK built-in -->
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>

<!-- Treat warnings as errors in Release builds -->
<TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<!-- Include BannedSymbols.txt as AdditionalFiles for BannedApiAnalyzers -->
<AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
</ItemGroup>

<ItemGroup>
<!-- Roslynator - Code quality and refactoring -->
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- AsyncFixer - Async/await best practices -->
<PackageReference Include="AsyncFixer" Version="2.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- Microsoft Threading Analyzers - Thread safety -->
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- BannedApiAnalyzers - Prevent usage of specific APIs -->
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- Meziantou - Comprehensive code quality -->
<PackageReference Include="Meziantou.Analyzer" Version="3.0.27">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- SonarAnalyzer - Industry-standard analysis -->
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.21.0.135717">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>