Add nightly Attack Surface Analyzer install-diff#40876
Draft
benhillis wants to merge 10 commits into
Draft
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a nightly “install-diff” security check using Attack Surface Analyzer (ASA) to compare a clean baseline vs. after installing the freshly built wsl.msi, then publishes SARIF + a net-new findings report (triaged via an allowlist) as part of the OneBranch nightly pipeline.
Changes:
- Introduces
tools/devops/Run-AsaInstallDiff.ps1to collect ASA snapshots, export SARIF/JSON, and filter results against an allowlist. - Adds
tools/devops/asa-expected-findings.jsonto allowlist known-benign ASA findings. - Adds a new
.pipelines/asa-stage.ymlstage and wires it into.pipelines/wsl-build-nightly-onebranch.ymlas non-gating initially.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tools/devops/Run-AsaInstallDiff.ps1 | New ASA install-diff runbook for collecting/exporting/triaging findings from MSI install. |
| tools/devops/asa-expected-findings.json | Allowlist of reviewed ASA findings to keep the nightly signal focused on net-new deltas. |
| .pipelines/wsl-build-nightly-onebranch.yml | Wires the new ASA stage into the nightly pipeline (non-gating for now). |
| .pipelines/asa-stage.yml | New OneBranch stage/job to download the MSI, run ASA diff, and publish results. |
| # --- [5] Filter findings through the allowlist --- | ||
| Write-Host '=== [5/5] Triage findings against allowlist ===' -ForegroundColor Cyan | ||
| $expected = Get-Content $ExpectedFindingsPath -Raw | ConvertFrom-Json | ||
| $sarif = Get-Content $sarifPath -Raw | ConvertFrom-Json -AsHashtable |
Comment on lines
+166
to
+168
| foreach ($r in $results) { | ||
| $ruleId = if ($r.ContainsKey('ruleId')) { [string]$r.ruleId } else { 'Default Level' } | ||
| $text = if ($r.ContainsKey('message') -and $r.message.ContainsKey('text')) { [string]$r.message.text } else { '' } |
Comment on lines
+124
to
+126
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir | ||
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir --outputsarif | ||
| if (-not (Test-Path $sarifPath)) { throw "Expected SARIF not produced at $sarifPath" } |
| # | ||
| # Attack Surface Analyzer (ASA) install-diff for the WSL MSI, intended to run on a | ||
| # clean Windows agent (CI nightly) but also usable locally. It: | ||
| # 1. (optionally) uninstalls any pre-existing WSL MSI to get a clean baseline, |
Comment on lines
+36
to
+39
| variables: | ||
| ob_outputDirectory: '$(Build.SourcesDirectory)\out' | ||
| ob_artifactBaseName: 'drop_wsl' | ||
| ob_artifactSuffix: '_asa' |
Comment on lines
+148
to
+152
| Write-Host '=== [4/5] Export diff (SARIF + JSON) ===' -ForegroundColor Cyan | ||
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir | ||
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir --outputsarif | ||
| if (-not (Test-Path $sarifPath)) { throw "Expected SARIF not produced at $sarifPath" } | ||
|
|
Comment on lines
+80
to
+85
| Write-Host '=== dotnet not found; bootstrapping .NET 8 SDK ===' -ForegroundColor Cyan | ||
| $installDir = Join-Path $env:USERPROFILE '.dotnet' | ||
| $installer = Join-Path $env:TEMP 'dotnet-install.ps1' | ||
| Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer -UseBasicParsing | ||
| & $installer -Channel 8.0 -InstallDir $installDir | ||
| if ($LASTEXITCODE -ne 0) { throw "dotnet bootstrap failed ($LASTEXITCODE)" } |
Comment on lines
+221
to
+223
| $expected = Get-Content $ExpectedFindingsPath -Raw | ConvertFrom-Json | ||
| $sarif = Get-Content $sarifPath -Raw | ConvertFrom-Json -AsHashtable | ||
| $results = $sarif.runs[0].results |
Comment on lines
+258
to
+260
| $ruleId = if ($r.ContainsKey('ruleId')) { [string]$r.ruleId } else { 'Default Level' } | ||
| $text = if ($r.ContainsKey('message') -and $r.message.ContainsKey('text')) { [string]$r.message.text } else { '' } | ||
| $path = Get-FindingPath $text |
Comment on lines
+148
to
+150
| $installer = Join-Path $env:TEMP 'dotnet-install.ps1' | ||
| Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer -UseBasicParsing | ||
| & $installer -Channel 8.0 -InstallDir $installDir |
| $installLog = Join-Path $WorkDir 'wsl-msi-install.log' | ||
| $sarifPath = Join-Path $WorkDir 'wsl_clean_vs_wsl_after_summary.Sarif' | ||
| $reportPath = Join-Path $WorkDir 'asa-net-new-findings.json' | ||
| $collectors = @('-c', '-C', '-d', '-F', '-p', '-r', '-s', '-u', '-f', '--directories', $InstallDir) |
| New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null | ||
| $zip = Join-Path $toolsDir "ASA_win_$AsaVersion.zip" | ||
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||
| Invoke-WebRequest -Uri $AsaUrl -OutFile $zip -UseBasicParsing |
Comment on lines
+190
to
+193
| Write-Host "Uninstalling existing WSL product $code" | ||
| $u = Start-Process msiexec.exe -ArgumentList @('/x', $code, '/qn', '/norestart') -Wait -PassThru | ||
| Write-Host " msiexec /x exit: $($u.ExitCode)" | ||
| } |
Comment on lines
+239
to
+241
| $expected = Get-Content $ExpectedFindingsPath -Raw | ConvertFrom-Json | ||
| $sarif = Get-Content $sarifPath -Raw | ConvertFrom-Json -AsHashtable | ||
| $results = $sarif.runs[0].results |
Comment on lines
+233
to
+235
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir | ||
| & $asa export-collect --firstrunid wsl_clean --secondrunid wsl_after --databasefilename $db --outputpath $WorkDir --outputsarif | ||
| if (-not (Test-Path $sarifPath)) { throw "Expected SARIF not produced at $sarifPath" } |
Comment on lines
+82
to
+84
| <ResultSummary outcome="Completed"> | ||
| <Counters total="1" executed="1" passed="$passed" failed="$failed" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" /> | ||
| </ResultSummary> |
Comment on lines
+150
to
+159
| function Install-AsaDotnetRuntime { | ||
| # ASA_win is a framework-dependent .NET 9 app. Install the ASP.NET Core 9 shared | ||
| # runtime (which carries the base .NET runtime too) into the default location so | ||
| # the apphost resolves it; also pin DOTNET_ROOT for deterministic resolution. | ||
| Write-Host "=== Installing .NET $AsaDotnetChannel runtime for ASA ===" -ForegroundColor Cyan | ||
| $installer = Join-Path $WorkDir 'dotnet-install.ps1' | ||
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||
| Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer -UseBasicParsing | ||
| & $installer -Channel $AsaDotnetChannel -Runtime aspnetcore -InstallDir $DotnetRoot | ||
| if ($LASTEXITCODE -ne 0) { throw "dotnet runtime bootstrap failed ($LASTEXITCODE)" } |
Comment on lines
+316
to
+318
| mkdir $(ob_outputDirectory)\testbin\devops | ||
| Copy-Item -Path "tools\devops\Run-AsaInstallDiff.ps1" -Destination "$(ob_outputDirectory)\testbin\devops\Run-AsaInstallDiff.ps1" | ||
| Copy-Item -Path "tools\devops\asa-expected-findings.json" -Destination "$(ob_outputDirectory)\testbin\devops\asa-expected-findings.json" |
Runs ASA against the freshly built wsl.msi on a clean agent to verify the installer does not weaken OS security configuration (Continuous SDL). Findings are triaged against a checked-in allowlist and published as SARIF. Non-gating initially. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uild container The OneBranch build container is network-isolated (NuGet 401) and is the wrong host for installing WSL and measuring OS attack surface. Move ASA to the nightly CloudTest harness: generate an ASA TestGroup/TestMap for one clean client image, stage the runbook + allowlist into the build drop, and run the install-diff via CloudTestServerBuildTask. Non-gating to start (ASA_FAIL_ON_NEW). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CloudTest's V2 schema rejects Type=Executable (session expansion failed with 'Executable is not a valid value for TestExecutionType'). Console runs an arbitrary process and reports pass/fail from its exit code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run-AsaInstallDiff.ps1 emits a TRX result file and exits non-zero on failure so CloudTest can parse per-test results. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
powershell.exe -File coerces args to strings and rejects [switch]/[bool] '1'/'0', so the runbook took the boolean flags as [string] and normalizes them. CloudTest only scans [WorkingDirectory]\TestResults for the TRX, so write the result file there instead of [LoggingDirectory]. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clean VM images have only a runtime-only dotnet host (no SDK), so dotnet tool install fails. Download the self-contained ASA_win zip (v2.3.321, the last tag with prebuilt binaries) instead; no SDK or NuGet feed required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uninstall registry keys without a DisplayName value threw under Set-StrictMode -Version Latest. Check the property exists before matching it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ASA_win 2.3.321 zip is framework-dependent on .NET 9 (NETCore + AspNetCore), which a clean VM lacks, so the collect failed with a missing hostpolicy.dll. Install the ASP.NET Core 9 runtime to the default dotnet location and pin DOTNET_ROOT before running ASA. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…XITCODE dotnet-install.ps1 is a PowerShell script, not an exe, so it never sets \0. Reading the unset variable under StrictMode threw right after a successful runtime install. Verify the install by checking for the Microsoft.NETCore.App and Microsoft.AspNetCore.App shared frameworks instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4873305 to
988e80c
Compare
Comment on lines
+244
to
+246
| $expected = Get-Content $ExpectedFindingsPath -Raw | ConvertFrom-Json | ||
| $sarif = Get-Content $sarifPath -Raw | ConvertFrom-Json -AsHashtable | ||
| $results = $sarif.runs[0].results |
Comment on lines
+141
to
+158
| # ASA ships a prebuilt Windows CLI zip (ASA_win), the last such build being 2.3.321 | ||
| # (newer tags ship the dotnet global tool only). That zip is framework-dependent on | ||
| # the .NET 9 runtimes (Microsoft.NETCore.App + Microsoft.AspNetCore.App), so on a | ||
| # clean VM image we also install the matching ASP.NET Core runtime before running it. | ||
| $AsaVersion = '2.3.321' | ||
| $AsaUrl = "https://github.com/microsoft/AttackSurfaceAnalyzer/releases/download/v$AsaVersion/ASA_win_$AsaVersion.zip" | ||
| $AsaDotnetChannel = '9.0' | ||
| $DotnetRoot = 'C:\Program Files\dotnet' | ||
|
|
||
| function Install-AsaDotnetRuntime { | ||
| # ASA_win is a framework-dependent .NET 9 app. Install the ASP.NET Core 9 shared | ||
| # runtime (which carries the base .NET runtime too) into the default location so | ||
| # the apphost resolves it; also pin DOTNET_ROOT for deterministic resolution. | ||
| Write-Host "=== Installing .NET $AsaDotnetChannel runtime for ASA ===" -ForegroundColor Cyan | ||
| $installer = Join-Path $WorkDir 'dotnet-install.ps1' | ||
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||
| Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer -UseBasicParsing | ||
| & $installer -Channel $AsaDotnetChannel -Runtime aspnetcore -InstallDir $DotnetRoot |
Comment on lines
+155
to
+158
| $installer = Join-Path $WorkDir 'dotnet-install.ps1' | ||
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||
| Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer -UseBasicParsing | ||
| & $installer -Channel $AsaDotnetChannel -Runtime aspnetcore -InstallDir $DotnetRoot |
Comment on lines
+121
to
+123
| if (-not (Test-Path $MsiPath)) { throw "MSI not found: $MsiPath" } | ||
| $MsiPath = (Resolve-Path $MsiPath).Path | ||
| New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a nightly Attack Surface Analyzer (ASA) install-diff that installs the built wsl.msi on a clean CloudTest VM and flags any net-new OS attack-surface changes against a reviewed allowlist. Non-gating to start. Satisfies the Continuous SDL ASA task.