From 2d5e0fb623087c662d8317cfa24311a498b0c427 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 5 Sep 2026 11:12:45 -0700 Subject: [PATCH] Investigate Windows release antivirus detections Scan exact public 0.1.0 and 0.1.1 artifacts on a disposable runner, without executing them or disabling protection. Preserve scanner versions and text evidence. AI assistance: prepared with Codex under Paul Bakaus direction. --- .github/workflows/defender-release-scan.yml | 30 +++++++ scripts/scan-windows-releases.ps1 | 96 +++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 .github/workflows/defender-release-scan.yml create mode 100644 scripts/scan-windows-releases.ps1 diff --git a/.github/workflows/defender-release-scan.yml b/.github/workflows/defender-release-scan.yml new file mode 100644 index 000000000..f5e0b2560 --- /dev/null +++ b/.github/workflows/defender-release-scan.yml @@ -0,0 +1,30 @@ +name: Investigate Windows release detections + +on: + workflow_dispatch: + push: + branches: [codex/scan-740-defender] + paths: + - .github/workflows/defender-release-scan.yml + - scripts/scan-windows-releases.ps1 + +permissions: + contents: read + +jobs: + defender: + runs-on: windows-2022 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Scan exact published samples without executing them + shell: pwsh + run: ./scripts/scan-windows-releases.ps1 + - name: Preserve text evidence only + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: defender-scan-evidence + path: ${{ runner.temp }}/impeccable-defender-evidence/ + if-no-files-found: warn + retention-days: 14 diff --git a/scripts/scan-windows-releases.ps1 b/scripts/scan-windows-releases.ps1 new file mode 100644 index 000000000..99de30426 --- /dev/null +++ b/scripts/scan-windows-releases.ps1 @@ -0,0 +1,96 @@ +# Maintainer-authorized diagnostic for #740. Never executes release binaries, +# adds exclusions, restores quarantine, or disables antivirus protection. +# A completed scan is evidence for this engine/definition/host only, not a +# false-positive determination or clearance for other machines. +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false +$evidence = Join-Path $env:RUNNER_TEMP 'impeccable-defender-evidence' +New-Item -ItemType Directory -Path $evidence -Force | Out-Null +$samples = Join-Path $env:RUNNER_TEMP ('impeccable-defender-samples-' + [guid]::NewGuid()) +New-Item -ItemType Directory -Path $samples | Out-Null +$report = [ordered]@{ + startedAt = (Get-Date).ToUniversalTime().ToString('o') + os = (Get-CimInstance Win32_OperatingSystem).Caption + status = 'initializing' + samples = @() +} +$failed = $false +try { + $before = Get-MpComputerStatus + $report.before = $before | Select-Object AMServiceEnabled, AntivirusEnabled, RealTimeProtectionEnabled, AMEngineVersion, AMProductVersion, AntivirusSignatureVersion, AntivirusSignatureLastUpdated + if (-not $before.AMServiceEnabled) { + # Enabling an installed service is safe on this disposable runner. Never + # change exclusion, remediation, cloud, or real-time protection policies. + Start-Service WinDefend + } + $mp = Get-ChildItem "$env:ProgramData\Microsoft\Windows Defender\Platform\*\MpCmdRun.exe" -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName + if (-not $mp) { $mp = "$env:ProgramFiles\Windows Defender\MpCmdRun.exe" } + if (-not (Test-Path -LiteralPath $mp)) { throw 'Microsoft Defender scanner is unavailable on this runner.' } + $report.scanner = $mp + $updateOutput = & $mp -SignatureUpdate 2>&1 | Out-String + $report.signatureUpdateExitCode = $LASTEXITCODE + $updateOutput | Set-Content (Join-Path $evidence 'signature-update.txt') + Write-Output $updateOutput + if ($report.signatureUpdateExitCode -ne 0) { throw 'Defender signature update failed; cannot claim a current-definition scan.' } + $current = Get-MpComputerStatus + $report.scannerStatus = $current | Select-Object AMServiceEnabled, AntivirusEnabled, RealTimeProtectionEnabled, AMEngineVersion, AMProductVersion, AntivirusSignatureVersion, AntivirusSignatureLastUpdated + if (-not $current.AMServiceEnabled -or -not $current.AntivirusEnabled) { throw 'Defender is not active; no scan verdict can be inferred.' } + $http = [System.Net.Http.HttpClient]::new() + $http.Timeout = [TimeSpan]::FromSeconds(90) + foreach ($sample in @( + @{ version = '0.1.0'; sha256 = 'a522fcf352b47f325facc3964b337a6d6d7d55e136440f1442e8013aad27f1d7' }, + @{ version = '0.1.1'; sha256 = '5d2f844a7f1dac3acdbac6035785043ab0cba6b81c1af97ba5c9cd1ecdd3dff8' } + )) { + $item = [ordered]@{ version = $sample.version; expectedSha256 = $sample.sha256; status = 'pending' } + $file = Join-Path $samples ("impeccable-" + $sample.version + '.exe') + try { + $url = "https://github.com/pbakaus/impeccable/releases/download/engine-v$($sample.version)/impeccable-windows-x64.exe" + $bytes = $http.GetByteArrayAsync($url).GetAwaiter().GetResult() + $item.actualSha256 = [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant() + $item.bytes = $bytes.Length + if ($item.actualSha256 -ne $sample.sha256) { throw 'Release bytes do not match the pinned investigation hash.' } + [System.IO.File]::WriteAllBytes($file, $bytes) + $bytes = $null + if (-not (Test-Path -LiteralPath $file)) { throw 'Sample disappeared after writing; inspect real-time detection evidence.' } + $item.authenticodeStatus = [string](Get-AuthenticodeSignature -LiteralPath $file).Status + # This suppresses remediation for this custom scan, not real-time + # protection. Detections appear in stdout; preserve it without guessing + # from exit 2 (which can mean either a detection or a scanning error). + $scanOutput = & $mp -Scan -ScanType 3 -File $file -DisableRemediation 2>&1 | Out-String + $item.scanExitCode = $LASTEXITCODE + $scanOutput | Set-Content (Join-Path $evidence ("scan-" + $sample.version + '.txt')) + Write-Output "Engine $($sample.version):" + Write-Output $scanOutput + $item.presentAfterScan = Test-Path -LiteralPath $file + $item.status = 'scan-finished-review-output' + if ($item.scanExitCode -ne 0) { $failed = $true } + } catch { + $item.status = 'unavailable-or-error' + $item.error = $_.Exception.Message + $failed = $true + } + $report.samples += $item + } + $http.Dispose() + $report.status = 'completed-review-evidence' +} catch { + $report.status = 'unavailable-or-error' + $report.error = $_.Exception.Message + $failed = $true +} finally { + try { + Get-MpThreatDetection | Select-Object InitialDetectionTime, ThreatID, Resources, ActionSuccess | + ConvertTo-Json -Depth 6 | Set-Content (Join-Path $evidence 'realtime-detections.json') + Get-MpThreat | Select-Object ThreatID, ThreatName, IsActive, DidThreatExecute | + ConvertTo-Json -Depth 6 | Set-Content (Join-Path $evidence 'threat-names.json') + } catch { $_.Exception.Message | Set-Content (Join-Path $evidence 'detection-query-error.txt') } + $report.finishedAt = (Get-Date).ToUniversalTime().ToString('o') + $json = $report | ConvertTo-Json -Depth 8 + $json | Set-Content (Join-Path $evidence 'report.json') + Write-Output $json + if ($env:GITHUB_STEP_SUMMARY) { + "## Defender investigation evidence`n`nThis is not a vendor false-positive determination.`n`n``````json`n$json`n``````" | Add-Content $env:GITHUB_STEP_SUMMARY + } +} +if ($failed) { exit 1 }