Skip to content

Add nightly Attack Surface Analyzer install-diff#40876

Draft
benhillis wants to merge 10 commits into
masterfrom
user/benhill/asa-install-diff
Draft

Add nightly Attack Surface Analyzer install-diff#40876
benhillis wants to merge 10 commits into
masterfrom
user/benhill/asa-install-diff

Conversation

@benhillis

@benhillis benhillis commented Jun 23, 2026

Copy link
Copy Markdown
Member

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.

Copilot AI review requested due to automatic review settings June 23, 2026 02:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ps1 to collect ASA snapshots, export SARIF/JSON, and filter results against an allowlist.
  • Adds tools/devops/asa-expected-findings.json to allowlist known-benign ASA findings.
  • Adds a new .pipelines/asa-stage.yml stage and wires it into .pipelines/wsl-build-nightly-onebranch.yml as 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 thread .pipelines/asa-stage.yml
Comment on lines +36 to +39
variables:
ob_outputDirectory: '$(Build.SourcesDirectory)\out'
ob_artifactBaseName: 'drop_wsl'
ob_artifactSuffix: '_asa'
Copilot AI review requested due to automatic review settings June 23, 2026 16:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

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 thread tools/devops/Run-AsaInstallDiff.ps1 Outdated
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)" }
Copilot AI review requested due to automatic review settings June 23, 2026 19:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

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 thread tools/devops/Run-AsaInstallDiff.ps1 Outdated
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
Copilot AI review requested due to automatic review settings June 23, 2026 22:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

$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)
Comment thread tools/devops/Run-AsaInstallDiff.ps1 Outdated
Comment thread tools/devops/Run-AsaInstallDiff.ps1 Outdated
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)"
}
Copilot AI review requested due to automatic review settings June 24, 2026 00:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

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 thread tools/devops/Run-AsaInstallDiff.ps1 Outdated
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 thread .pipelines/build-job.yml
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"
Ben Hillis and others added 10 commits June 24, 2026 15:49
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>
Copilot AI review requested due to automatic review settings June 24, 2026 22:49
@benhillis benhillis force-pushed the user/benhill/asa-install-diff branch from 4873305 to 988e80c Compare June 24, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants