Skip to content

Merge pull request #200 from Chris-Wolfgang/protected/alert-sha-pinning #259

Merge pull request #200 from Chris-Wolfgang/protected/alert-sha-pinning

Merge pull request #200 from Chris-Wolfgang/protected/alert-sha-pinning #259

Workflow file for this run

name: "CodeQL Security Analysis"
on:
push:
branches: [ "main" ]
pull_request:
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
languages: ${{ matrix.language }}
# security-extended adds the broader security query pack on top of the
# default queries (more rules, slightly longer scans).
queries: security-extended
- name: Setup .NET
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: '10.0.x'
- name: Restore .NET workloads
if: steps.check-csharp.outputs.has-csharp == 'true'
shell: pwsh
run: |
# Skip entirely if no csproj declares a workload-bearing TFM (android/ios/
# maccatalyst/maui/tvos/tizen/browser) — netX.Y-windows TFMs use
# SDK-bundled projection assemblies, not the workload installer, so
# they're intentionally excluded. Saves ~5-15s on pure-library
# repos and removes a network-dependent failure mode.
$hasWorkloadTfm = @(Get-ChildItem -Recurse -Filter *.csproj |
Select-String -Pattern 'net\d+\.\d+-(android|ios|maccatalyst|maui|tvos|tizen|browser)' -List).Count -gt 0
if (-not $hasWorkloadTfm) {
Write-Host "No workload-bearing TFMs — skipping dotnet workload restore"
exit 0
}
$solution = Get-ChildItem -Path . -Recurse -Depth 2 -Include "*.sln", "*.slnx" | Select-Object -First 1
if ($solution) {
Write-Host "Restoring workloads for $($solution.FullName)"
dotnet workload restore "$($solution.FullName)"
} else {
Write-Host "No solution found; restoring workloads for all projects"
dotnet workload restore
}
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet workload restore failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
- 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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
category: "/language:${{matrix.language}}"
- name: Complete Security Scan
if: always()
shell: pwsh
# Step outcomes come in via env, not ${{ }} interpolation into the
# script body (zizmor template-injection).
env:
CHECK_CSHARP_OUTCOME: ${{ steps.check-csharp.outcome }}
HAS_CSHARP: ${{ steps.check-csharp.outputs.has-csharp }}
BUILD_OUTCOME: ${{ steps.build.outcome }}
CODEQL_OUTCOME: ${{ steps.perform-codeql-analysis.outcome }}
run: |
Write-Host "=== CodeQL Security Scan Complete ===" -ForegroundColor Cyan
# Check the outcome of previous steps
$checkCsharpOutcome = $env:CHECK_CSHARP_OUTCOME
$hasCsharp = $env:HAS_CSHARP
$buildOutcome = $env:BUILD_OUTCOME
$codeqlOutcome = $env:CODEQL_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