From a24e1bff15e3428fd1be19366b433d2361023324 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 19:06:11 +0000 Subject: [PATCH 01/48] Initial plan From 86840bb88e63e6a357d613b41ac59478e257fd34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 19:16:16 +0000 Subject: [PATCH 02/48] Add Attack Surface Analyzer test to windows packaging workflow Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .../workflows/windows-packaging-reusable.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 5a763544c62..37f3f245955 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -71,6 +71,18 @@ jobs: Invoke-CIInstall -SkipUser shell: pwsh + - name: Install Attack Surface Analyzer + run: | + Write-Host "Installing Attack Surface Analyzer..." + dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI + shell: pwsh + + - name: Take Baseline Snapshot + run: | + Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -a + shell: pwsh + - name: Build and Package run: | Import-Module .\tools\ci.psm1 @@ -78,6 +90,31 @@ jobs: Invoke-CIFinish -Runtime ${{ matrix.runtimePrefix }}-${{ matrix.architecture }} -channel ${{ matrix.channel }} shell: pwsh + - name: Test Package Installation + run: | + Write-Host "Finding and installing PowerShell packages for testing..." + $packages = Get-ChildItem -Path "${{ github.workspace }}/artifacts" -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue + if ($packages) { + foreach ($package in $packages) { + Write-Host "Installing package for ASA test: $($package.FullName)" + Start-Process msiexec.exe -ArgumentList "/i", "`"$($package.FullName)`"", "/quiet", "/norestart" -Wait -NoNewWindow + } + } else { + Write-Host "No MSI packages found to test." + } + shell: pwsh + + - name: Analyze with Attack Surface Analyzer + run: | + Write-Host "Taking post-installation snapshot..." + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -a + + Write-Host "Exporting comparison results..." + & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect + + Write-Host "Attack Surface Analyzer test completed." + shell: pwsh + - name: Upload Build Artifacts if: always() uses: actions/upload-artifact@v4 From ed49207ead60d88b540a5ebf3265349f0c9e866f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 19:25:32 +0000 Subject: [PATCH 03/48] Remove redundant MSI installation step - Invoke-CIFinish already installs MSI Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 37f3f245955..08b972bfc2b 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -90,20 +90,6 @@ jobs: Invoke-CIFinish -Runtime ${{ matrix.runtimePrefix }}-${{ matrix.architecture }} -channel ${{ matrix.channel }} shell: pwsh - - name: Test Package Installation - run: | - Write-Host "Finding and installing PowerShell packages for testing..." - $packages = Get-ChildItem -Path "${{ github.workspace }}/artifacts" -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue - if ($packages) { - foreach ($package in $packages) { - Write-Host "Installing package for ASA test: $($package.FullName)" - Start-Process msiexec.exe -ArgumentList "/i", "`"$($package.FullName)`"", "/quiet", "/norestart" -Wait -NoNewWindow - } - } else { - Write-Host "No MSI packages found to test." - } - shell: pwsh - - name: Analyze with Attack Surface Analyzer run: | Write-Host "Taking post-installation snapshot..." From 2641bc43c3f4d0cb552df955f70c1d59c700e3c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:22:08 +0000 Subject: [PATCH 04/48] Add windows-packaging-reusable.yml to path filters for packagingChanged Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/actions/infrastructure/path-filters/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/infrastructure/path-filters/action.yml b/.github/actions/infrastructure/path-filters/action.yml index 09ed7c22d17..da7b1324af7 100644 --- a/.github/actions/infrastructure/path-filters/action.yml +++ b/.github/actions/infrastructure/path-filters/action.yml @@ -90,6 +90,7 @@ runs: const packagingChanged = files.some(file => file.filename === '.github/workflows/windows-ci.yml' || + file.filename === '.github/workflows/windows-packaging-reusable.yml' || file.filename.startsWith('assets/wix/') || file.filename === 'PowerShell.Common.props' || file.filename.match(/^src\/.*\.csproj$/) || From f8ded685fe81526c6f0da529b221e2f98ef66ccb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:53:12 +0000 Subject: [PATCH 05/48] Pin Attack Surface Analyzer version to 2.4.176 Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 3afd88da067..a3877342af0 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -74,7 +74,7 @@ jobs: - name: Install Attack Surface Analyzer run: | Write-Host "Installing Attack Surface Analyzer..." - dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI + dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.4.176 shell: pwsh - name: Take Baseline Snapshot From aeb83d3cedb179c6f0380e3b996b6f4d0c4ecf4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:47:05 +0000 Subject: [PATCH 06/48] Update Attack Surface Analyzer version to 2.3.328 Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index a3877342af0..1fdb9f35cc9 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -74,7 +74,7 @@ jobs: - name: Install Attack Surface Analyzer run: | Write-Host "Installing Attack Surface Analyzer..." - dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.4.176 + dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 shell: pwsh - name: Take Baseline Snapshot From dfc6268425203e72f1754f6417cfe3832cbacc9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:31:42 +0000 Subject: [PATCH 07/48] Optimize ASA collection for faster execution with targeted collectors and directories Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 1fdb9f35cc9..58caefc694d 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -80,7 +80,14 @@ jobs: - name: Take Baseline Snapshot run: | Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -a + # Use specific collectors for faster execution: + # - File: Monitor filesystem changes (limited to Program Files) + # - Service: Monitor Windows services + # - Registry: Monitor registry changes + # - User: Monitor user account changes + # - Port: Monitor network port changes + # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, EventLog, Firewall, COM, Certificates + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" shell: pwsh - name: Build and Package @@ -93,7 +100,7 @@ jobs: - name: Analyze with Attack Surface Analyzer run: | Write-Host "Taking post-installation snapshot..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -a + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" Write-Host "Exporting comparison results..." & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect From 9ab76e00dd4a6f333952f0242dea3baebd1ef803 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:38:21 +0000 Subject: [PATCH 08/48] Add C:\Windows monitoring and EventLog collector to ASA configuration Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 58caefc694d..2df43dfec79 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -81,13 +81,14 @@ jobs: run: | Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." # Use specific collectors for faster execution: - # - File: Monitor filesystem changes (limited to Program Files) + # - File: Monitor filesystem changes (PowerShell install dirs and Windows) # - Service: Monitor Windows services # - Registry: Monitor registry changes # - User: Monitor user account changes # - Port: Monitor network port changes - # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, EventLog, Firewall, COM, Certificates - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" + # - EventLog: Monitor Windows event logs + # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, Firewall, COM, Certificates + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --collect EventLog --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" --directories "C:\Windows" shell: pwsh - name: Build and Package @@ -100,7 +101,7 @@ jobs: - name: Analyze with Attack Surface Analyzer run: | Write-Host "Taking post-installation snapshot..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --collect EventLog --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" --directories "C:\Windows" Write-Host "Exporting comparison results..." & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect From a659f5d36ab987a1c5a14734f205cc75773ddd4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:04:40 +0000 Subject: [PATCH 09/48] Fix ASA command syntax to use correct collector flags and comma-separated directories Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 2df43dfec79..898d57d677a 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -81,14 +81,14 @@ jobs: run: | Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." # Use specific collectors for faster execution: - # - File: Monitor filesystem changes (PowerShell install dirs and Windows) - # - Service: Monitor Windows services - # - Registry: Monitor registry changes - # - User: Monitor user account changes - # - Port: Monitor network port changes - # - EventLog: Monitor Windows event logs + # -f: File system collector (PowerShell install dirs and Windows) + # -s: Service collector + # -r: Registry collector + # -u: User and group account collector + # -p: Network port collector + # -l: Log collector (EventLog) # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, Firewall, COM, Certificates - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --collect EventLog --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" --directories "C:\Windows" + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,C:\Windows" shell: pwsh - name: Build and Package @@ -101,7 +101,7 @@ jobs: - name: Analyze with Attack Surface Analyzer run: | Write-Host "Taking post-installation snapshot..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect --collect File --collect Service --collect Registry --collect User --collect Port --collect EventLog --directories "C:\Program Files\PowerShell" --directories "C:\Program Files (x86)\PowerShell" --directories "C:\Windows" + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,C:\Windows" Write-Host "Exporting comparison results..." & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect From 43975f36dcc2b5696893ab23974da0d5d6386723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:26:57 +0000 Subject: [PATCH 10/48] Narrow Windows monitoring to System32\PowerShell directory for faster execution Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 898d57d677a..42893eae1ae 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -81,14 +81,15 @@ jobs: run: | Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." # Use specific collectors for faster execution: - # -f: File system collector (PowerShell install dirs and Windows) + # -f: File system collector (PowerShell install dirs and System32\PowerShell) # -s: Service collector # -r: Registry collector # -u: User and group account collector # -p: Network port collector # -l: Log collector (EventLog) # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, Firewall, COM, Certificates - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,C:\Windows" + $windowsPowerShellPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell" + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,$windowsPowerShellPath" shell: pwsh - name: Build and Package @@ -101,7 +102,8 @@ jobs: - name: Analyze with Attack Surface Analyzer run: | Write-Host "Taking post-installation snapshot..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,C:\Windows" + $windowsPowerShellPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell" + & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,$windowsPowerShellPath" Write-Host "Exporting comparison results..." & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect From 6bf0707e608b4fbf88b034e2c36a58b20ec127b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:46:51 +0000 Subject: [PATCH 11/48] Upload Attack Surface Analyzer results as artifacts for analysis Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 42893eae1ae..035ce32b5ce 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -111,6 +111,16 @@ jobs: Write-Host "Attack Surface Analyzer test completed." shell: pwsh + - name: Upload Attack Surface Analyzer Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: attack-surface-analyzer-results-${{ matrix.architecture }}-${{ matrix.channel }} + path: | + ${{ github.workspace }}/*_summary.json.txt + ${{ github.workspace }}/*_results.json.txt + ${{ github.workspace }}/asa.sqlite + - name: Upload Build Artifacts if: always() uses: actions/upload-artifact@v4 From 25a9001faa29d2aae86f03c9e58abc3fa89e9fb6 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 21 Oct 2025 16:52:12 -0700 Subject: [PATCH 12/48] Apply suggestion from @TravisEz13 --- .github/workflows/windows-packaging-reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 035ce32b5ce..eb616419870 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -106,7 +106,7 @@ jobs: & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,$windowsPowerShellPath" Write-Host "Exporting comparison results..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect + & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase Write-Host "Attack Surface Analyzer test completed." shell: pwsh From 0315cbd47ba65c9f2b277f6c637026662d06d67d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:21:59 +0000 Subject: [PATCH 13/48] Add SARIF output file to ASA results artifact upload Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index eb616419870..2e8737db29e 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -119,6 +119,7 @@ jobs: path: | ${{ github.workspace }}/*_summary.json.txt ${{ github.workspace }}/*_results.json.txt + ${{ github.workspace }}/*.sarif ${{ github.workspace }}/asa.sqlite - name: Upload Build Artifacts From fbce5e9bac6830b2dd75e6e50b4eb120c512a733 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:19:11 +0000 Subject: [PATCH 14/48] Run ASA only for x64/stable matrix configuration Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/workflows/windows-packaging-reusable.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 2e8737db29e..fb7773630cb 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -24,6 +24,10 @@ jobs: - architecture: x64 channel: preview runtimePrefix: win7 + - architecture: x64 + channel: stable + runtimePrefix: win7 + runASA: true - architecture: x86 channel: stable runtimePrefix: win7 @@ -72,12 +76,14 @@ jobs: shell: pwsh - name: Install Attack Surface Analyzer + if: matrix.runASA == true run: | Write-Host "Installing Attack Surface Analyzer..." dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 shell: pwsh - name: Take Baseline Snapshot + if: matrix.runASA == true run: | Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." # Use specific collectors for faster execution: @@ -100,6 +106,7 @@ jobs: shell: pwsh - name: Analyze with Attack Surface Analyzer + if: matrix.runASA == true run: | Write-Host "Taking post-installation snapshot..." $windowsPowerShellPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell" @@ -112,7 +119,7 @@ jobs: shell: pwsh - name: Upload Attack Surface Analyzer Results - if: always() + if: matrix.runASA == true && always() uses: actions/upload-artifact@v4 with: name: attack-surface-analyzer-results-${{ matrix.architecture }}-${{ matrix.channel }} From de2d81fed182c93e053259166602bc5c97af0335 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:36:13 +0000 Subject: [PATCH 15/48] Move ASA to Windows container for clean baseline testing Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .../workflows/windows-packaging-reusable.yml | 103 ++++++++++++------ 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 15e756fd105..9de4942a815 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -76,29 +76,6 @@ jobs: Invoke-CIInstall -SkipUser shell: pwsh - - name: Install Attack Surface Analyzer - if: matrix.runASA == true - run: | - Write-Host "Installing Attack Surface Analyzer..." - dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 - shell: pwsh - - - name: Take Baseline Snapshot - if: matrix.runASA == true - run: | - Write-Host "Taking baseline snapshot with Attack Surface Analyzer..." - # Use specific collectors for faster execution: - # -f: File system collector (PowerShell install dirs and System32\PowerShell) - # -s: Service collector - # -r: Registry collector - # -u: User and group account collector - # -p: Network port collector - # -l: Log collector (EventLog) - # Exclude slow collectors like WiFi, TPM, Cryptographic Keys, Firewall, COM, Certificates - $windowsPowerShellPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell" - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,$windowsPowerShellPath" - shell: pwsh - - name: Build and Package run: | Import-Module .\tools\ci.psm1 @@ -106,17 +83,78 @@ jobs: Invoke-CIFinish -Runtime ${{ matrix.runtimePrefix }}-${{ matrix.architecture }} -channel ${{ matrix.channel }} shell: pwsh - - name: Analyze with Attack Surface Analyzer + - name: Run Attack Surface Analyzer in Container if: matrix.runASA == true run: | - Write-Host "Taking post-installation snapshot..." - $windowsPowerShellPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell" - & "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell,$windowsPowerShellPath" - - Write-Host "Exporting comparison results..." - & "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase - - Write-Host "Attack Surface Analyzer test completed." + Write-Host "Running Attack Surface Analyzer in clean Windows container..." + + # Find the MSI file that was built + $msiPath = Get-ChildItem -Path "$env:SYSTEM_ARTIFACTSDIRECTORY" -Filter "*.msi" -Recurse | Select-Object -First 1 -ExpandProperty FullName + if (-not $msiPath) { + throw "Could not find MSI file in artifacts directory" + } + Write-Host "Found MSI: $msiPath" + + # Create a directory for container volume mount + $containerWorkDir = "$env:TEMP\asa-container-work" + New-Item -ItemType Directory -Force -Path $containerWorkDir | Out-Null + + # Copy MSI to container work directory + $msiFileName = Split-Path $msiPath -Leaf + Copy-Item $msiPath -Destination "$containerWorkDir\$msiFileName" + + # Create PowerShell script to run inside container + $scriptLines = @( + '# Install .NET tool (ASA)', + 'Write-Host "Installing Attack Surface Analyzer in container..."', + 'dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328', + '$env:PATH += ";$env:USERPROFILE\.dotnet\tools"', + '', + '# Take baseline snapshot', + 'Write-Host "Taking baseline snapshot..."', + '& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell"', + '', + '# Install the MSI', + 'Write-Host "Installing PowerShell MSI..."', + '$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName', + 'Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow', + '', + '# Take post-installation snapshot', + 'Write-Host "Taking post-installation snapshot..."', + '& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell"', + '', + '# Export results', + 'Write-Host "Exporting comparison results..."', + '& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase', + '', + '# Copy results to work directory', + 'Write-Host "Copying results to work directory..."', + 'Copy-Item -Path "*.txt" -Destination C:\work\ -ErrorAction SilentlyContinue', + 'Copy-Item -Path "*.sarif" -Destination C:\work\ -ErrorAction SilentlyContinue', + 'Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue', + '', + 'Write-Host "Attack Surface Analyzer test completed in container."' + ) + + $scriptLines | Set-Content -Path "$containerWorkDir\run-asa.ps1" + + # Run container with volume mount + Write-Host "Starting Windows container..." + docker run --rm ` + --isolation process ` + -v "${containerWorkDir}:C:\work" ` + mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 ` + powershell -ExecutionPolicy Bypass -File C:\work\run-asa.ps1 + + # Copy results back to workspace + Write-Host "Copying results from container work directory to workspace..." + Copy-Item -Path "$containerWorkDir\*_summary.json.txt" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue + Copy-Item -Path "$containerWorkDir\*_results.json.txt" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue + Copy-Item -Path "$containerWorkDir\*.sarif" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue + Copy-Item -Path "$containerWorkDir\asa.sqlite" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue + Copy-Item -Path "$containerWorkDir\install.log" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue + + Write-Host "Attack Surface Analyzer container test completed successfully." shell: pwsh - name: Upload Attack Surface Analyzer Results @@ -138,3 +176,4 @@ jobs: path: | ${{ github.workspace }}/artifacts/**/* !${{ github.workspace }}/artifacts/**/*.pdb + From 8b472da2b9c0884acafe0f803b77b567814c8237 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 16:49:50 -0800 Subject: [PATCH 16/48] Add Dockerfile and PowerShell script for Attack Surface Analyzer testing --- tools/AttackSurfaceAnalyzer/Dockerfile | 131 ++++++++ tools/AttackSurfaceAnalyzer/README.md | 153 +++++++++ .../Run-AttackSurfaceAnalyzer.ps1 | 315 ++++++++++++++++++ 3 files changed, 599 insertions(+) create mode 100644 tools/AttackSurfaceAnalyzer/Dockerfile create mode 100644 tools/AttackSurfaceAnalyzer/README.md create mode 100644 tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 diff --git a/tools/AttackSurfaceAnalyzer/Dockerfile b/tools/AttackSurfaceAnalyzer/Dockerfile new file mode 100644 index 00000000000..383e13ca4c9 --- /dev/null +++ b/tools/AttackSurfaceAnalyzer/Dockerfile @@ -0,0 +1,131 @@ +# Dockerfile for Attack Surface Analyzer Testing +# This builds a container image with Attack Surface Analyzer pre-installed +# for testing PowerShell MSI installations + +# Use Windows Server Core with .NET SDK as base image +FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 + +# Set shell to PowerShell for easier scripting +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# Install Attack Surface Analyzer as a global .NET tool +RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 + +# Add .NET tools directory to PATH +RUN $env:PATH += ';C:/Users/ContainerAdministrator/.dotnet/tools'; \ + [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine) + +# Set working directory +WORKDIR C:/work + +# Create a helper script for running ASA tests +RUN @' \ +# Helper script for running Attack Surface Analyzer tests \ +param( \ + [Parameter(Mandatory=$true)] \ + [string]$MsiPath \ +) \ +\ +$ErrorActionPreference = ''Stop'' \ +\ +Write-Host ''========================================='' \ +Write-Host ''Attack Surface Analyzer Test Runner'' \ +Write-Host ''========================================='' \ +Write-Host '''' \ +Write-Host ''MSI Path: $MsiPath'' \ +Write-Host '''' \ +\ +# Verify ASA is available \ +Write-Host ''Verifying Attack Surface Analyzer installation...'' \ +& asa --version \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error ''Attack Surface Analyzer is not properly installed'' \ + exit 1 \ +} \ +\ +# Take baseline snapshot \ +Write-Host '''' \ +Write-Host ''Taking baseline snapshot...'' \ +Write-Host ''========================================='' \ +& asa collect -f -s -r -u -p -l --directories ''C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'' \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error ''Failed to take baseline snapshot'' \ + exit 1 \ +} \ +Write-Host ''Baseline snapshot completed'' \ +\ +# Install the MSI \ +Write-Host '''' \ +Write-Host ''Installing PowerShell MSI...'' \ +Write-Host ''========================================='' \ +if (-not (Test-Path $MsiPath)) { \ + Write-Error ''MSI file not found: $MsiPath'' \ + exit 1 \ +} \ +\ +$logPath = ''C:\work\install.log'' \ +Write-Host ''Running: msiexec.exe /i $MsiPath /quiet /norestart /l*v $logPath'' \ +$process = Start-Process msiexec.exe -ArgumentList ''/i'', $MsiPath, ''/quiet'', ''/norestart'', ''/l*v'', $logPath -Wait -NoNewWindow -PassThru \ +Write-Host ''MSI installation completed with exit code: $($process.ExitCode)'' \ +\ +if ($process.ExitCode -ne 0) { \ + Write-Warning ''MSI installation returned non-zero exit code. Check install.log for details.'' \ +} \ +\ +# Take post-installation snapshot \ +Write-Host '''' \ +Write-Host ''Taking post-installation snapshot...'' \ +Write-Host ''========================================='' \ +& asa collect -f -s -r -u -p -l --directories ''C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'' \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error ''Failed to take post-installation snapshot'' \ + exit 1 \ +} \ +Write-Host ''Post-installation snapshot completed'' \ +\ +# Export results \ +Write-Host '''' \ +Write-Host ''Exporting comparison results...'' \ +Write-Host ''========================================='' \ +& asa export-collect --outputsarif --savetodatabase \ +if ($LASTEXITCODE -ne 0) { \ + Write-Warning ''Export completed with exit code: $LASTEXITCODE'' \ +} \ +\ +# List and copy results to work directory \ +Write-Host '''' \ +Write-Host ''Copying results to work directory...'' \ +Write-Host ''========================================='' \ +$resultFiles = Get-ChildItem -Path . -Include ''*.txt'', ''*.sarif'', ''asa.sqlite'' -Recurse \ +foreach ($file in $resultFiles) { \ + Copy-Item -Path $file.FullName -Destination C:\work\ -Force -ErrorAction SilentlyContinue \ + Write-Host ''Copied: $($file.Name)'' \ +} \ +\ +Write-Host '''' \ +Write-Host ''========================================='' \ +Write-Host ''Attack Surface Analyzer test completed!'' \ +Write-Host ''========================================='' \ +Write-Host ''Results are available in C:\work directory'' \ +'@ | Out-File -FilePath C:\Scripts\Run-ASA-Test.ps1 -Encoding utf8 + +# Create Scripts directory +RUN New-Item -ItemType Directory -Path C:\Scripts -Force | Out-Null + +# Default command shows help +CMD ["powershell", "-NoProfile", "-Command", \ + "Write-Host 'Attack Surface Analyzer Container'; \ + Write-Host ''; \ + Write-Host 'Usage:'; \ + Write-Host ' docker run --rm --isolation process -v \":C:\\work\" powershell -File C:\\Scripts\\Run-ASA-Test.ps1 -MsiPath C:\\work\\'; \ + Write-Host ''; \ + Write-Host 'Example:'; \ + Write-Host ' docker run --rm --isolation process -v \"C:\\temp:C:\\work\" asa-test powershell -File C:\\Scripts\\Run-ASA-Test.ps1 -MsiPath C:\\work\\PowerShell.msi'; \ + Write-Host ''; \ + Write-Host 'The local path should contain the MSI file to test.'; \ + Write-Host 'Results will be written back to the same directory.'"] + +# Label for documentation +LABEL description="Windows container for running Attack Surface Analyzer tests on PowerShell MSI installations" +LABEL version="1.0" +LABEL maintainer="PowerShell Team" diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md new file mode 100644 index 00000000000..f8e611076a7 --- /dev/null +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -0,0 +1,153 @@ +# Attack Surface Analyzer Testing + +This directory contains tools for running Attack Surface Analyzer (ASA) tests on PowerShell MSI installations using Docker. + +## Overview + +Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a system's attack surface. These scripts allow you to run ASA tests locally in a clean Windows container to analyze what changes when PowerShell is installed. + +## Files + +- **Run-AttackSurfaceAnalyzer.ps1** - PowerShell script to run ASA tests locally +- **Dockerfile** - Dockerfile for building a container image with ASA pre-installed +- **README.md** - This documentation file + +## Prerequisites + +- Windows 10/11 or Windows Server +- Docker Desktop with Windows containers enabled +- PowerShell 5.1 or later +- A built PowerShell MSI file to test + +## Quick Start + +### Option 1: Using the PowerShell Script (Recommended) + +The simplest way to run ASA tests is using the provided PowerShell script: + +```powershell +# Run with automatic MSI detection +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 + +# Run with specific MSI file +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" + +# Specify output directory for results +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" -OutputPath "C:\results" + +# Keep the temporary work directory for debugging +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -KeepWorkDirectory +``` + +The script will: +1. Find or use the specified MSI file +2. Create a temporary work directory +3. Start a Windows container +4. Install Attack Surface Analyzer in the container +5. Take a baseline snapshot +6. Install the PowerShell MSI +7. Take a post-installation snapshot +8. Export comparison results +9. Copy results back to your specified output directory + +### Option 2: Using the Dockerfile + +If you prefer to build a custom image with ASA pre-installed: + +```powershell +# Build the Docker image +docker build -f tools\AttackSurfaceAnalyzer\Dockerfile -t powershell-asa-test . + +# Run the container with your MSI +docker run --rm --isolation process ` + -v "C:\path\to\msi\directory:C:\work" ` + powershell-asa-test ` + powershell -File C:\Scripts\Run-ASA-Test.ps1 -MsiPath C:\work\PowerShell.msi +``` + +## Output Files + +The test will generate several output files: + +- **`*_summary.json.txt`** - Summary of detected changes +- **`*_results.json.txt`** - Detailed results in JSON format +- **`*.sarif`** - SARIF format results (can be viewed in VS Code) +- **`asa.sqlite`** - SQLite database with full analysis data +- **`install.log`** - MSI installation log file + +## Analyzing Results + +### Using VS Code + +The SARIF files can be opened directly in VS Code with the SARIF Viewer extension to see a formatted view of the findings. + +### Using PowerShell + +```powershell +# Read the summary file +Get-Content "*_summary.json.txt" | ConvertFrom-Json | Format-List + +# Query the SQLite database (requires SQLite tools) +# Example: List all file changes +# sqlite3 asa.sqlite "SELECT * FROM file_system WHERE change_type != 'NONE'" +``` + +## Troubleshooting + +### Docker Not Available + +If you get an error that Docker is not available: +1. Install Docker Desktop from https://www.docker.com/products/docker-desktop +2. Ensure Docker is running +3. Switch to Windows containers (right-click Docker tray icon → "Switch to Windows containers") + +### Container Fails to Start + +- Ensure you have enough disk space (containers can be large) +- Check that Windows containers are enabled in Docker settings +- Try pulling the base image manually: `docker pull mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022` + +### No Results Generated + +- Check the install.log file for MSI installation errors +- Run with `-KeepWorkDirectory` to inspect the temporary work directory +- Verify the MSI file is valid and not corrupted + +## Advanced Usage + +### Custom Container Image + +You can specify a different container image: + +```powershell +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 ` + -ContainerImage "mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2022" +``` + +### Debugging + +To debug issues, keep the work directory and examine the files: + +```powershell +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -KeepWorkDirectory + +# The script will print the work directory path +# You can then examine: +# - run-asa.ps1 - The script that runs in the container +# - install.log - MSI installation log +# - Any other generated files +``` + +## Integration with CI/CD + +These tools were extracted from the GitHub Actions workflow to allow local testing. If you need to integrate ASA testing back into a CI/CD pipeline, you can: + +1. Use the PowerShell script directly in your pipeline +2. Build and push the Docker image to a registry +3. Use the Dockerfile as a base for custom testing scenarios + +## More Information + +- [Attack Surface Analyzer on GitHub](https://github.com/microsoft/AttackSurfaceAnalyzer) +- [Docker for Windows Documentation](https://docs.docker.com/desktop/windows/) +- [SARIF Documentation](https://sarifweb.azurewebsites.net/) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 new file mode 100644 index 00000000000..59a4940b76a --- /dev/null +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -0,0 +1,315 @@ +<# +.SYNOPSIS + Run Attack Surface Analyzer test locally using Docker to analyze PowerShell MSI installation. + +.DESCRIPTION + This script runs Attack Surface Analyzer in a clean Windows container to analyze + the attack surface changes when installing PowerShell MSI. It takes a baseline + snapshot, installs the MSI, takes a post-installation snapshot, and exports the + comparison results. + +.PARAMETER MsiPath + Path to the PowerShell MSI file to test. If not provided, the script will search + for an MSI in the artifacts directory. + +.PARAMETER OutputPath + Directory where results will be saved. Defaults to current directory. + +.PARAMETER ContainerImage + Docker container image to use. Defaults to mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 + +.PARAMETER KeepWorkDirectory + If specified, keeps the temporary work directory after the test completes. + +.EXAMPLE + .\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" + +.EXAMPLE + .\Run-AttackSurfaceAnalyzer.ps1 -OutputPath "C:\results" + +.NOTES + Requires Docker Desktop with Windows containers enabled. +#> + +[CmdletBinding()] +param( + [Parameter()] + [string]$MsiPath, + + [Parameter()] + [string]$OutputPath = $PWD, + + [Parameter()] + [string]$ContainerImage = "mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022", + + [Parameter()] + [switch]$KeepWorkDirectory +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $color = switch ($Level) { + "ERROR" { "Red" } + "WARNING" { "Yellow" } + "SUCCESS" { "Green" } + default { "White" } + } + Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color +} + +function Test-DockerAvailable { + try { + $null = docker version 2>&1 + return $true + } + catch { + return $false + } +} + +# Verify Docker is available +Write-Log "Checking Docker availability..." +if (-not (Test-DockerAvailable)) { + Write-Log "Docker is not available. Please install Docker Desktop and ensure it's running with Windows containers enabled." -Level ERROR + exit 1 +} + +# Find MSI if not provided +if (-not $MsiPath) { + Write-Log "No MSI path provided, searching in artifacts directory..." + $possiblePaths = @( + "$PSScriptRoot\..\artifacts", + "$PSScriptRoot\..\" + ) + + foreach ($path in $possiblePaths) { + if (Test-Path $path) { + $msiFiles = Get-ChildItem -Path $path -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue + if ($msiFiles) { + $MsiPath = $msiFiles[0].FullName + Write-Log "Found MSI: $MsiPath" -Level SUCCESS + break + } + } + } + + if (-not $MsiPath) { + Write-Log "Could not find MSI file. Please specify -MsiPath parameter." -Level ERROR + exit 1 + } +} + +# Verify MSI exists +if (-not (Test-Path $MsiPath)) { + Write-Log "MSI file not found: $MsiPath" -Level ERROR + exit 1 +} + +$MsiPath = Resolve-Path $MsiPath +Write-Log "Using MSI: $MsiPath" + +# Create output directory +$OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) +if (-not (Test-Path $OutputPath)) { + New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null + Write-Log "Created output directory: $OutputPath" +} + +# Create container work directory +$containerWorkDir = Join-Path $env:TEMP "asa-container-work-$(Get-Date -Format 'yyyyMMdd-HHmmss')" +New-Item -ItemType Directory -Force -Path $containerWorkDir | Out-Null +Write-Log "Created container work directory: $containerWorkDir" + +try { + # Copy MSI to container work directory + $msiFileName = Split-Path $MsiPath -Leaf + $destMsiPath = Join-Path $containerWorkDir $msiFileName + Write-Log "Copying MSI to work directory..." + Copy-Item $MsiPath -Destination $destMsiPath + + # Create PowerShell script to run inside container + Write-Log "Creating container execution script..." + $scriptContent = @' +# Install .NET tool (ASA) +Write-Host "=========================================" +Write-Host "Installing Attack Surface Analyzer..." +Write-Host "=========================================" +dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install Attack Surface Analyzer" + exit 1 +} +$env:PATH += ";$env:USERPROFILE\.dotnet\tools" + +# Verify ASA is available +Write-Host "" +Write-Host "Verifying ASA installation..." +& "$env:USERPROFILE\.dotnet\tools\asa.exe" --version + +# Take baseline snapshot +Write-Host "" +Write-Host "=========================================" +Write-Host "Taking baseline snapshot..." +Write-Host "=========================================" +& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to take baseline snapshot" + exit 1 +} + +# Install the MSI +Write-Host "" +Write-Host "=========================================" +Write-Host "Installing PowerShell MSI..." +Write-Host "=========================================" +$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName +Write-Host "MSI file: $msiFile" +Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow +if ($LASTEXITCODE -ne 0) { + Write-Warning "MSI installation returned exit code: $LASTEXITCODE" + Write-Host "Check install.log for details" +} + +# Take post-installation snapshot +Write-Host "" +Write-Host "=========================================" +Write-Host "Taking post-installation snapshot..." +Write-Host "=========================================" +& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to take post-installation snapshot" + exit 1 +} + +# Export results +Write-Host "" +Write-Host "=========================================" +Write-Host "Exporting comparison results..." +Write-Host "=========================================" +& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to export results with exit code: $LASTEXITCODE" +} + +# Copy results to work directory +Write-Host "" +Write-Host "=========================================" +Write-Host "Copying results to work directory..." +Write-Host "=========================================" +Get-ChildItem -Path "*.txt" | ForEach-Object { + Write-Host "Copying: $($_.Name)" + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue +} +Get-ChildItem -Path "*.sarif" | ForEach-Object { + Write-Host "Copying: $($_.Name)" + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue +} +if (Test-Path "asa.sqlite") { + Write-Host "Copying: asa.sqlite" + Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue +} + +Write-Host "" +Write-Host "=========================================" +Write-Host "Attack Surface Analyzer test completed!" +Write-Host "=========================================" +'@ + + $scriptContent | Set-Content -Path (Join-Path $containerWorkDir "run-asa.ps1") -Encoding UTF8 + + # Build Dockerfile content for reference + Write-Log "Creating Dockerfile for reference..." + $dockerfileContent = @" +# Dockerfile for Attack Surface Analyzer Testing +# This file is created for reference and can be used to build a custom image +# if you prefer not to use the inline script approach + +FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 + +# Install Attack Surface Analyzer +RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 + +# Add tools to PATH +ENV PATH="\${PATH};C:/Users/ContainerAdministrator/.dotnet/tools" + +WORKDIR C:/work + +# The container expects: +# - MSI file to be mounted to C:/work +# - Script to be mounted to C:/work/run-asa.ps1 +# Run with: docker run --rm --isolation process -v "path:C:/work" powershell -ExecutionPolicy Bypass -File C:/work/run-asa.ps1 +"@ + + $dockerfileContent | Set-Content -Path (Join-Path $containerWorkDir "Dockerfile") -Encoding UTF8 + + # Run container with volume mount + Write-Log "=========================================" -Level SUCCESS + Write-Log "Starting Windows container..." -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS + Write-Log "Container image: $ContainerImage" + Write-Log "This may take several minutes..." + Write-Host "" + + docker run --rm ` + --isolation process ` + -v "${containerWorkDir}:C:\work" ` + $ContainerImage ` + powershell -ExecutionPolicy Bypass -File C:\work\run-asa.ps1 + + if ($LASTEXITCODE -ne 0) { + Write-Log "Container execution failed with exit code: $LASTEXITCODE" -Level WARNING + } + + # Copy results to output directory + Write-Host "" + Write-Log "=========================================" -Level SUCCESS + Write-Log "Copying results to output directory..." -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS + + $resultFiles = @( + "*_summary.json.txt", + "*_results.json.txt", + "*.sarif", + "asa.sqlite", + "install.log" + ) + + $copiedCount = 0 + foreach ($pattern in $resultFiles) { + $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue + foreach ($file in $files) { + $destPath = Join-Path $OutputPath $file.Name + Copy-Item -Path $file.FullName -Destination $destPath -Force + Write-Log "Copied: $($file.Name) -> $destPath" -Level SUCCESS + $copiedCount++ + } + } + + if ($copiedCount -eq 0) { + Write-Log "Warning: No result files found to copy" -Level WARNING + } + else { + Write-Log "Copied $copiedCount result file(s)" -Level SUCCESS + } + + Write-Host "" + Write-Log "=========================================" -Level SUCCESS + Write-Log "Attack Surface Analyzer test completed!" -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS + Write-Log "Results saved to: $OutputPath" -Level SUCCESS +} +finally { + # Cleanup + if (-not $KeepWorkDirectory) { + Write-Log "Cleaning up temporary work directory..." + Remove-Item -Path $containerWorkDir -Recurse -Force -ErrorAction SilentlyContinue + Write-Log "Cleanup completed" + } + else { + Write-Log "Work directory preserved at: $containerWorkDir" -Level SUCCESS + } +} From fe088e82bd90b05b200ffc5ebf2f456985713b34 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 16:50:14 -0800 Subject: [PATCH 17/48] Remove Attack Surface Analyzer steps from Windows packaging workflow --- .../workflows/windows-packaging-reusable.yml | 86 ------------------- 1 file changed, 86 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 9de4942a815..99940d7b2c0 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -27,7 +27,6 @@ jobs: - architecture: x64 channel: stable runtimePrefix: win7 - runASA: true - architecture: x86 channel: stable runtimePrefix: win7 @@ -83,91 +82,6 @@ jobs: Invoke-CIFinish -Runtime ${{ matrix.runtimePrefix }}-${{ matrix.architecture }} -channel ${{ matrix.channel }} shell: pwsh - - name: Run Attack Surface Analyzer in Container - if: matrix.runASA == true - run: | - Write-Host "Running Attack Surface Analyzer in clean Windows container..." - - # Find the MSI file that was built - $msiPath = Get-ChildItem -Path "$env:SYSTEM_ARTIFACTSDIRECTORY" -Filter "*.msi" -Recurse | Select-Object -First 1 -ExpandProperty FullName - if (-not $msiPath) { - throw "Could not find MSI file in artifacts directory" - } - Write-Host "Found MSI: $msiPath" - - # Create a directory for container volume mount - $containerWorkDir = "$env:TEMP\asa-container-work" - New-Item -ItemType Directory -Force -Path $containerWorkDir | Out-Null - - # Copy MSI to container work directory - $msiFileName = Split-Path $msiPath -Leaf - Copy-Item $msiPath -Destination "$containerWorkDir\$msiFileName" - - # Create PowerShell script to run inside container - $scriptLines = @( - '# Install .NET tool (ASA)', - 'Write-Host "Installing Attack Surface Analyzer in container..."', - 'dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328', - '$env:PATH += ";$env:USERPROFILE\.dotnet\tools"', - '', - '# Take baseline snapshot', - 'Write-Host "Taking baseline snapshot..."', - '& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell"', - '', - '# Install the MSI', - 'Write-Host "Installing PowerShell MSI..."', - '$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName', - 'Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow', - '', - '# Take post-installation snapshot', - 'Write-Host "Taking post-installation snapshot..."', - '& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell"', - '', - '# Export results', - 'Write-Host "Exporting comparison results..."', - '& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase', - '', - '# Copy results to work directory', - 'Write-Host "Copying results to work directory..."', - 'Copy-Item -Path "*.txt" -Destination C:\work\ -ErrorAction SilentlyContinue', - 'Copy-Item -Path "*.sarif" -Destination C:\work\ -ErrorAction SilentlyContinue', - 'Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue', - '', - 'Write-Host "Attack Surface Analyzer test completed in container."' - ) - - $scriptLines | Set-Content -Path "$containerWorkDir\run-asa.ps1" - - # Run container with volume mount - Write-Host "Starting Windows container..." - docker run --rm ` - --isolation process ` - -v "${containerWorkDir}:C:\work" ` - mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 ` - powershell -ExecutionPolicy Bypass -File C:\work\run-asa.ps1 - - # Copy results back to workspace - Write-Host "Copying results from container work directory to workspace..." - Copy-Item -Path "$containerWorkDir\*_summary.json.txt" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue - Copy-Item -Path "$containerWorkDir\*_results.json.txt" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue - Copy-Item -Path "$containerWorkDir\*.sarif" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue - Copy-Item -Path "$containerWorkDir\asa.sqlite" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue - Copy-Item -Path "$containerWorkDir\install.log" -Destination $env:GITHUB_WORKSPACE -ErrorAction SilentlyContinue - - Write-Host "Attack Surface Analyzer container test completed successfully." - shell: pwsh - - - name: Upload Attack Surface Analyzer Results - if: matrix.runASA == true && always() - uses: actions/upload-artifact@v4 - with: - name: attack-surface-analyzer-results-${{ matrix.architecture }}-${{ matrix.channel }} - path: | - ${{ github.workspace }}/*_summary.json.txt - ${{ github.workspace }}/*_results.json.txt - ${{ github.workspace }}/*.sarif - ${{ github.workspace }}/asa.sqlite - - name: Upload Build Artifacts if: always() uses: actions/upload-artifact@v5 From e5a165cf718037dbbcf52555c0adfe9b07828db5 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 16:54:34 -0800 Subject: [PATCH 18/48] Enhance README and script to clarify MSI handling and build process --- tools/AttackSurfaceAnalyzer/README.md | 36 ++++-- .../Run-AttackSurfaceAnalyzer.ps1 | 122 +++++++++++++++--- 2 files changed, 125 insertions(+), 33 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index f8e611076a7..541a3dd492d 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -17,7 +17,14 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst - Windows 10/11 or Windows Server - Docker Desktop with Windows containers enabled - PowerShell 5.1 or later -- A built PowerShell MSI file to test +- (Optional) A pre-built PowerShell MSI file to test, or the script will build one for you + +### Build Prerequisites (if not providing -MsiPath) + +If you want the script to build the MSI automatically, ensure you have: +- .NET SDK (as specified in global.json) +- All PowerShell build dependencies (the script will use Start-PSBuild and Start-PSPackage) +- See the main PowerShell README for full build prerequisites ## Quick Start @@ -26,29 +33,34 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst The simplest way to run ASA tests is using the provided PowerShell script: ```powershell -# Run with automatic MSI detection +# Build MSI and run ASA test automatically (default behavior) .\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -# Run with specific MSI file +# Run with specific MSI file (skips build) .\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" +# Search for existing MSI without building +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -NoBuild + # Specify output directory for results -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" -OutputPath "C:\results" +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -OutputPath "C:\results" # Keep the temporary work directory for debugging .\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -KeepWorkDirectory ``` The script will: + +1. Build PowerShell MSI (if not provided via -MsiPath or -NoBuild) 1. Find or use the specified MSI file -2. Create a temporary work directory -3. Start a Windows container -4. Install Attack Surface Analyzer in the container -5. Take a baseline snapshot -6. Install the PowerShell MSI -7. Take a post-installation snapshot -8. Export comparison results -9. Copy results back to your specified output directory +1. Create a temporary work directory +1. Start a Windows container +1. Install Attack Surface Analyzer in the container +1. Take a baseline snapshot +1. Install the PowerShell MSI +1. Take a post-installation snapshot +1. Export comparison results +1. Copy results back to your specified output directory ### Option 2: Using the Dockerfile diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 59a4940b76a..0fa0bf6ae44 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -9,8 +9,11 @@ comparison results. .PARAMETER MsiPath - Path to the PowerShell MSI file to test. If not provided, the script will search - for an MSI in the artifacts directory. + Path to the PowerShell MSI file to test. If not provided, the script will build + a new MSI using Start-PSBuild. + +.PARAMETER NoBuild + Skip building the MSI and only search for existing MSI files. .PARAMETER OutputPath Directory where results will be saved. Defaults to current directory. @@ -27,15 +30,23 @@ .EXAMPLE .\Run-AttackSurfaceAnalyzer.ps1 -OutputPath "C:\results" +.EXAMPLE + .\Run-AttackSurfaceAnalyzer.ps1 -NoBuild + .NOTES Requires Docker Desktop with Windows containers enabled. + If MsiPath is not provided and NoBuild is not specified, the script will + import build.psm1 and build a new MSI package. #> -[CmdletBinding()] +[CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [string]$MsiPath, + [Parameter()] + [switch]$NoBuild, + [Parameter()] [string]$OutputPath = $PWD, @@ -78,28 +89,97 @@ if (-not (Test-DockerAvailable)) { exit 1 } -# Find MSI if not provided +# Find or build MSI if not provided if (-not $MsiPath) { - Write-Log "No MSI path provided, searching in artifacts directory..." - $possiblePaths = @( - "$PSScriptRoot\..\artifacts", - "$PSScriptRoot\..\" - ) - - foreach ($path in $possiblePaths) { - if (Test-Path $path) { - $msiFiles = Get-ChildItem -Path $path -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue - if ($msiFiles) { - $MsiPath = $msiFiles[0].FullName - Write-Log "Found MSI: $MsiPath" -Level SUCCESS - break + if ($NoBuild) { + Write-Log "No MSI path provided, searching in artifacts directory..." + $possiblePaths = @( + "$PSScriptRoot\..\..\artifacts", + "$PSScriptRoot\..\..\" + ) + + foreach ($path in $possiblePaths) { + if (Test-Path $path) { + $msiFiles = Get-ChildItem -Path $path -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue + if ($msiFiles) { + $MsiPath = $msiFiles[0].FullName + Write-Log "Found MSI: $MsiPath" -Level SUCCESS + break + } } } + + if (-not $MsiPath) { + Write-Log "Could not find MSI file. Please specify -MsiPath parameter or remove -NoBuild to build a new MSI." -Level ERROR + exit 1 + } } - - if (-not $MsiPath) { - Write-Log "Could not find MSI file. Please specify -MsiPath parameter." -Level ERROR - exit 1 + else { + # Build the MSI + Write-Log "No MSI path provided, building PowerShell MSI..." -Level SUCCESS + + # Find the repository root + $repoRoot = $PSScriptRoot + while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { + $repoRoot = Split-Path $repoRoot -Parent + } + + if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { + Write-Log "Could not find build.psm1. Please run this script from the PowerShell repository." -Level ERROR + exit 1 + } + + Write-Log "Repository root: $repoRoot" + + try { + # Import build module + Write-Log "Importing build.psm1..." + Import-Module (Join-Path $repoRoot "build.psm1") -Force + + # Build PowerShell + Write-Log "Starting PowerShell build (this may take several minutes)..." + $buildOutput = Join-Path $repoRoot "out" + Start-PSBuild -Configuration Release -Output $buildOutput + + if ($LASTEXITCODE -ne 0) { + Write-Log "Build failed with exit code: $LASTEXITCODE" -Level ERROR + exit 1 + } + + Write-Log "Build completed successfully" -Level SUCCESS + + # Package the MSI + Write-Log "Creating MSI package..." + Start-PSPackage -Type msi -WindowsRuntime win7-x64 + + if ($LASTEXITCODE -ne 0) { + Write-Log "Packaging failed with exit code: $LASTEXITCODE" -Level ERROR + exit 1 + } + + Write-Log "MSI packaging completed successfully" -Level SUCCESS + + # Find the newly created MSI + $artifactsPath = Join-Path $repoRoot "artifacts" + if (Test-Path $artifactsPath) { + $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending + if ($msiFiles) { + $MsiPath = $msiFiles[0].FullName + Write-Log "Built MSI: $MsiPath" -Level SUCCESS + } + } + + if (-not $MsiPath) { + Write-Log "MSI was built but could not be found in artifacts directory" -Level ERROR + exit 1 + } + } + catch { + Write-Log "Error during build: $_" -Level ERROR + Write-Log $_.ScriptStackTrace -Level ERROR + exit 1 + } } } From db789979893838f450d1e0c9fd6e0773e69d5c59 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 17:00:02 -0800 Subject: [PATCH 19/48] Enhance Docker installation process in README and script to support automatic installation via winget --- tools/AttackSurfaceAnalyzer/README.md | 7 +- .../Run-AttackSurfaceAnalyzer.ps1 | 95 +++++++++++++++---- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index 541a3dd492d..190e4bbc76f 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -108,11 +108,16 @@ Get-Content "*_summary.json.txt" | ConvertFrom-Json | Format-List ### Docker Not Available -If you get an error that Docker is not available: +If Docker is not installed, the script will prompt you to install it automatically using winget. + +To install manually: + 1. Install Docker Desktop from https://www.docker.com/products/docker-desktop 2. Ensure Docker is running 3. Switch to Windows containers (right-click Docker tray icon → "Switch to Windows containers") +**Automatic Installation**: If you run the script without Docker installed, it will ask if you want to install Docker Desktop using `winget install docker.dockerdesktop`. After installation completes, restart Docker Desktop and run the script again. + ### Container Fails to Start - Ensure you have enough disk space (containers can be large) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 0fa0bf6ae44..cf5bc35308b 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -35,8 +35,11 @@ .NOTES Requires Docker Desktop with Windows containers enabled. + If Docker is not installed, the script will prompt to install it using winget. If MsiPath is not provided and NoBuild is not specified, the script will import build.psm1 and build a new MSI package. + + Supports -WhatIf and -Confirm for Docker installation. #> [CmdletBinding(SupportsShouldProcess)] @@ -82,11 +85,67 @@ function Test-DockerAvailable { } } +function Test-WingetAvailable { + try { + $null = Get-Command winget -ErrorAction Stop + return $true + } + catch { + return $false + } +} + +function Install-DockerDesktop { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact="High")] + param() + + if (-not (Test-WingetAvailable)) { + Write-Log "winget is not available. Please install winget (App Installer from Microsoft Store) or install Docker Desktop manually from https://www.docker.com/products/docker-desktop" -Level ERROR + return $false + } + + if ($PSCmdlet.ShouldProcess("Docker Desktop", "Install using winget")) { + Write-Log "Installing Docker Desktop using winget..." -Level SUCCESS + Write-Log "This may take several minutes..." + + try { + winget install docker.dockerdesktop --accept-package-agreements --accept-source-agreements + + if ($LASTEXITCODE -eq 0) { + Write-Log "Docker Desktop installed successfully!" -Level SUCCESS + Write-Log "Please restart Docker Desktop and ensure Windows containers are enabled, then run this script again." -Level SUCCESS + return $true + } + else { + Write-Log "Docker Desktop installation failed with exit code: $LASTEXITCODE" -Level ERROR + return $false + } + } + catch { + Write-Log "Error installing Docker Desktop: $_" -Level ERROR + return $false + } + } + else { + Write-Log "Docker Desktop installation cancelled by user." -Level WARNING + return $false + } +} + # Verify Docker is available Write-Log "Checking Docker availability..." if (-not (Test-DockerAvailable)) { - Write-Log "Docker is not available. Please install Docker Desktop and ensure it's running with Windows containers enabled." -Level ERROR - exit 1 + Write-Log "Docker is not available." -Level WARNING + Write-Log "Docker Desktop is required to run Attack Surface Analyzer tests in containers." -Level WARNING + + if (Install-DockerDesktop) { + Write-Log "Docker Desktop has been installed. Please restart Docker Desktop and run this script again." -Level SUCCESS + exit 0 + } + else { + Write-Log "Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop and ensure it's running with Windows containers enabled." -Level ERROR + exit 1 + } } # Find or build MSI if not provided @@ -97,7 +156,7 @@ if (-not $MsiPath) { "$PSScriptRoot\..\..\artifacts", "$PSScriptRoot\..\..\" ) - + foreach ($path in $possiblePaths) { if (Test-Path $path) { $msiFiles = Get-ChildItem -Path $path -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue @@ -108,7 +167,7 @@ if (-not $MsiPath) { } } } - + if (-not $MsiPath) { Write-Log "Could not find MSI file. Please specify -MsiPath parameter or remove -NoBuild to build a new MSI." -Level ERROR exit 1 @@ -117,59 +176,59 @@ if (-not $MsiPath) { else { # Build the MSI Write-Log "No MSI path provided, building PowerShell MSI..." -Level SUCCESS - + # Find the repository root $repoRoot = $PSScriptRoot while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { $repoRoot = Split-Path $repoRoot -Parent } - + if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { Write-Log "Could not find build.psm1. Please run this script from the PowerShell repository." -Level ERROR exit 1 } - + Write-Log "Repository root: $repoRoot" - + try { # Import build module Write-Log "Importing build.psm1..." Import-Module (Join-Path $repoRoot "build.psm1") -Force - + # Build PowerShell Write-Log "Starting PowerShell build (this may take several minutes)..." $buildOutput = Join-Path $repoRoot "out" Start-PSBuild -Configuration Release -Output $buildOutput - + if ($LASTEXITCODE -ne 0) { Write-Log "Build failed with exit code: $LASTEXITCODE" -Level ERROR exit 1 } - + Write-Log "Build completed successfully" -Level SUCCESS - + # Package the MSI Write-Log "Creating MSI package..." Start-PSPackage -Type msi -WindowsRuntime win7-x64 - + if ($LASTEXITCODE -ne 0) { Write-Log "Packaging failed with exit code: $LASTEXITCODE" -Level ERROR exit 1 } - + Write-Log "MSI packaging completed successfully" -Level SUCCESS - + # Find the newly created MSI $artifactsPath = Join-Path $repoRoot "artifacts" if (Test-Path $artifactsPath) { - $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | + $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending if ($msiFiles) { $MsiPath = $msiFiles[0].FullName Write-Log "Built MSI: $MsiPath" -Level SUCCESS } } - + if (-not $MsiPath) { Write-Log "MSI was built but could not be found in artifacts directory" -Level ERROR exit 1 @@ -349,7 +408,7 @@ WORKDIR C:/work Write-Log "=========================================" -Level SUCCESS Write-Log "Copying results to output directory..." -Level SUCCESS Write-Log "=========================================" -Level SUCCESS - + $resultFiles = @( "*_summary.json.txt", "*_results.json.txt", From b4bb32d836cd2a1fa6ce1dafd4a79a18e18ba8f1 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 17:02:55 -0800 Subject: [PATCH 20/48] Enhance Docker handling in README and script to automate installation and startup --- tools/AttackSurfaceAnalyzer/README.md | 15 ++- .../Run-AttackSurfaceAnalyzer.ps1 | 119 ++++++++++++++++-- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index 190e4bbc76f..7b42fdb882b 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -108,16 +108,23 @@ Get-Content "*_summary.json.txt" | ConvertFrom-Json | Format-List ### Docker Not Available -If Docker is not installed, the script will prompt you to install it automatically using winget. +The script automatically handles Docker Desktop installation and startup: -To install manually: +**If Docker Desktop is installed but not running:** +- The script will automatically start Docker Desktop for you +- It waits up to 60 seconds for Docker to become available +- You'll be prompted for confirmation (supports `-Confirm` and `-WhatIf`) + +**If Docker Desktop is not installed:** +- The script will prompt you to install it automatically using winget +- After installation completes, start Docker Desktop and run the script again + +**Manual Installation:** 1. Install Docker Desktop from https://www.docker.com/products/docker-desktop 2. Ensure Docker is running 3. Switch to Windows containers (right-click Docker tray icon → "Switch to Windows containers") -**Automatic Installation**: If you run the script without Docker installed, it will ask if you want to install Docker Desktop using `winget install docker.dockerdesktop`. After installation completes, restart Docker Desktop and run the script again. - ### Container Fails to Start - Ensure you have enough disk space (containers can be large) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index cf5bc35308b..4763c24812f 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -35,11 +35,17 @@ .NOTES Requires Docker Desktop with Windows containers enabled. - If Docker is not installed, the script will prompt to install it using winget. - If MsiPath is not provided and NoBuild is not specified, the script will - import build.psm1 and build a new MSI package. - Supports -WhatIf and -Confirm for Docker installation. + Docker Desktop Handling: + - If Docker Desktop is installed but not running, the script will start it automatically + - If Docker Desktop is not installed, the script will prompt to install it using winget + - Waits up to 60 seconds for Docker to become available after starting + + Build Behavior: + - If MsiPath is not provided and NoBuild is not specified, the script will + import build.psm1 and build a new MSI package + + Supports -WhatIf and -Confirm for Docker installation and startup. #> [CmdletBinding(SupportsShouldProcess)] @@ -85,6 +91,79 @@ function Test-DockerAvailable { } } +function Test-DockerDesktopInstalled { + # Check if Docker Desktop executable exists + $dockerDesktopPaths = @( + "${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe", + "${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe", + "${env:LOCALAPPDATA}\Programs\Docker\Docker Desktop.exe" + ) + + foreach ($path in $dockerDesktopPaths) { + if (Test-Path $path) { + return $path + } + } + return $null +} + +function Test-DockerDesktopRunning { + $process = Get-Process -Name "Docker Desktop" -ErrorAction SilentlyContinue + return $null -ne $process +} + +function Start-DockerDesktopApp { + [CmdletBinding(SupportsShouldProcess)] + param() + + $dockerDesktopPath = Test-DockerDesktopInstalled + + if (-not $dockerDesktopPath) { + Write-Log "Docker Desktop executable not found." -Level ERROR + return $false + } + + if (Test-DockerDesktopRunning) { + Write-Log "Docker Desktop is already running." -Level SUCCESS + return $true + } + + if ($PSCmdlet.ShouldProcess("Docker Desktop", "Start application")) { + Write-Log "Starting Docker Desktop..." -Level SUCCESS + Write-Log "This may take a minute for Docker to fully start..." + + try { + Start-Process -FilePath $dockerDesktopPath -WindowStyle Hidden + + # Wait for Docker to become available (up to 60 seconds) + $maxWaitSeconds = 60 + $waitedSeconds = 0 + + while ($waitedSeconds -lt $maxWaitSeconds) { + Start-Sleep -Seconds 5 + $waitedSeconds += 5 + Write-Log "Waiting for Docker to start... ($waitedSeconds/$maxWaitSeconds seconds)" + + if (Test-DockerAvailable) { + Write-Log "Docker Desktop started successfully!" -Level SUCCESS + return $true + } + } + + Write-Log "Docker Desktop was started but is not responding yet. Please wait a moment and try again." -Level WARNING + return $false + } + catch { + Write-Log "Error starting Docker Desktop: $_" -Level ERROR + return $false + } + } + else { + Write-Log "Starting Docker Desktop cancelled by user." -Level WARNING + return $false + } +} + function Test-WingetAvailable { try { $null = Get-Command winget -ErrorAction Stop @@ -135,16 +214,34 @@ function Install-DockerDesktop { # Verify Docker is available Write-Log "Checking Docker availability..." if (-not (Test-DockerAvailable)) { - Write-Log "Docker is not available." -Level WARNING - Write-Log "Docker Desktop is required to run Attack Surface Analyzer tests in containers." -Level WARNING + Write-Log "Docker is not responding." -Level WARNING - if (Install-DockerDesktop) { - Write-Log "Docker Desktop has been installed. Please restart Docker Desktop and run this script again." -Level SUCCESS - exit 0 + # Check if Docker Desktop is installed but not running + if (Test-DockerDesktopInstalled) { + Write-Log "Docker Desktop is installed but not running." -Level WARNING + + if (Start-DockerDesktopApp) { + Write-Log "Docker Desktop is now running and ready." -Level SUCCESS + } + else { + Write-Log "Failed to start Docker Desktop or it's taking longer than expected." -Level ERROR + Write-Log "Please start Docker Desktop manually and ensure Windows containers are enabled, then run this script again." -Level ERROR + exit 1 + } } else { - Write-Log "Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop and ensure it's running with Windows containers enabled." -Level ERROR - exit 1 + # Docker Desktop is not installed + Write-Log "Docker Desktop is not installed." -Level WARNING + Write-Log "Docker Desktop is required to run Attack Surface Analyzer tests in containers." -Level WARNING + + if (Install-DockerDesktop) { + Write-Log "Docker Desktop has been installed. Please restart Docker Desktop and run this script again." -Level SUCCESS + exit 0 + } + else { + Write-Log "Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop and ensure it's running with Windows containers enabled." -Level ERROR + exit 1 + } } } From a9bf342fe6bb8f335c3888d96d6fb34bb84c430b Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 17:10:01 -0800 Subject: [PATCH 21/48] Refactor Run-AttackSurfaceAnalyzer.ps1 for improved logging and error handling --- .../Run-AttackSurfaceAnalyzer.ps1 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 4763c24812f..93cf17699f4 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -292,10 +292,21 @@ if (-not $MsiPath) { Write-Log "Importing build.psm1..." Import-Module (Join-Path $repoRoot "build.psm1") -Force + # Import packaging module + Write-Log "Importing packaging module..." + $packagingModulePath = Join-Path $repoRoot "tools\packaging\packaging.psm1" + if (Test-Path $packagingModulePath) { + Import-Module $packagingModulePath -Force + } + else { + Write-Log "Could not find packaging.psm1 at: $packagingModulePath" -Level ERROR + exit 1 + } + # Build PowerShell Write-Log "Starting PowerShell build (this may take several minutes)..." - $buildOutput = Join-Path $repoRoot "out" - Start-PSBuild -Configuration Release -Output $buildOutput + Write-Log "Running: Start-PSBuild -Runtime win7-x64 -Configuration Release" + Start-PSBuild -Runtime win7-x64 -Configuration Release -ErrorAction Stop if ($LASTEXITCODE -ne 0) { Write-Log "Build failed with exit code: $LASTEXITCODE" -Level ERROR @@ -306,7 +317,8 @@ if (-not $MsiPath) { # Package the MSI Write-Log "Creating MSI package..." - Start-PSPackage -Type msi -WindowsRuntime win7-x64 + Write-Log "Running: Start-PSPackage -Type msi -WindowsRuntime win7-x64" + Start-PSPackage -Type msi -WindowsRuntime win7-x64 -SkipReleaseChecks if ($LASTEXITCODE -ne 0) { Write-Log "Packaging failed with exit code: $LASTEXITCODE" -Level ERROR From 8f8e9163f0413e362c5e7b49e69f55d84dfc93cb Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 17:32:52 -0800 Subject: [PATCH 22/48] Improve MSI detection logic to check both repo root and artifacts directory --- .../Run-AttackSurfaceAnalyzer.ps1 | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 93cf17699f4..561e7bf4d9d 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -327,19 +327,30 @@ if (-not $MsiPath) { Write-Log "MSI packaging completed successfully" -Level SUCCESS - # Find the newly created MSI - $artifactsPath = Join-Path $repoRoot "artifacts" - if (Test-Path $artifactsPath) { - $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending - if ($msiFiles) { - $MsiPath = $msiFiles[0].FullName - Write-Log "Built MSI: $MsiPath" -Level SUCCESS + # Find the newly created MSI at the repo root + Write-Log "Looking for MSI at repo root: $repoRoot" + $msiFiles = Get-ChildItem -Path $repoRoot -Filter "*.msi" -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending + if ($msiFiles) { + $MsiPath = $msiFiles[0].FullName + Write-Log "Built MSI: $MsiPath" -Level SUCCESS + } + else { + # Also check artifacts directory as fallback + Write-Log "MSI not found at repo root, checking artifacts directory..." + $artifactsPath = Join-Path $repoRoot "artifacts" + if (Test-Path $artifactsPath) { + $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending + if ($msiFiles) { + $MsiPath = $msiFiles[0].FullName + Write-Log "Found MSI in artifacts: $MsiPath" -Level SUCCESS + } } } if (-not $MsiPath) { - Write-Log "MSI was built but could not be found in artifacts directory" -Level ERROR + Write-Log "MSI was built but could not be found in repo root or artifacts directory" -Level ERROR exit 1 } } From 15da07c9cc45f838665aae3eabf5bbe24e752866 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 17:46:11 -0800 Subject: [PATCH 23/48] Refactor Docker setup: move Dockerfile to a dedicated subfolder and update scripts to use the static Dockerfile for building the Attack Surface Analyzer container. --- tools/AttackSurfaceAnalyzer/Dockerfile | 131 --------------- tools/AttackSurfaceAnalyzer/README.md | 17 +- .../Run-AttackSurfaceAnalyzer.ps1 | 149 ++++-------------- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 117 ++++++++++++++ 4 files changed, 156 insertions(+), 258 deletions(-) delete mode 100644 tools/AttackSurfaceAnalyzer/Dockerfile create mode 100644 tools/AttackSurfaceAnalyzer/docker/Dockerfile diff --git a/tools/AttackSurfaceAnalyzer/Dockerfile b/tools/AttackSurfaceAnalyzer/Dockerfile deleted file mode 100644 index 383e13ca4c9..00000000000 --- a/tools/AttackSurfaceAnalyzer/Dockerfile +++ /dev/null @@ -1,131 +0,0 @@ -# Dockerfile for Attack Surface Analyzer Testing -# This builds a container image with Attack Surface Analyzer pre-installed -# for testing PowerShell MSI installations - -# Use Windows Server Core with .NET SDK as base image -FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 - -# Set shell to PowerShell for easier scripting -SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] - -# Install Attack Surface Analyzer as a global .NET tool -RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 - -# Add .NET tools directory to PATH -RUN $env:PATH += ';C:/Users/ContainerAdministrator/.dotnet/tools'; \ - [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine) - -# Set working directory -WORKDIR C:/work - -# Create a helper script for running ASA tests -RUN @' \ -# Helper script for running Attack Surface Analyzer tests \ -param( \ - [Parameter(Mandatory=$true)] \ - [string]$MsiPath \ -) \ -\ -$ErrorActionPreference = ''Stop'' \ -\ -Write-Host ''========================================='' \ -Write-Host ''Attack Surface Analyzer Test Runner'' \ -Write-Host ''========================================='' \ -Write-Host '''' \ -Write-Host ''MSI Path: $MsiPath'' \ -Write-Host '''' \ -\ -# Verify ASA is available \ -Write-Host ''Verifying Attack Surface Analyzer installation...'' \ -& asa --version \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error ''Attack Surface Analyzer is not properly installed'' \ - exit 1 \ -} \ -\ -# Take baseline snapshot \ -Write-Host '''' \ -Write-Host ''Taking baseline snapshot...'' \ -Write-Host ''========================================='' \ -& asa collect -f -s -r -u -p -l --directories ''C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'' \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error ''Failed to take baseline snapshot'' \ - exit 1 \ -} \ -Write-Host ''Baseline snapshot completed'' \ -\ -# Install the MSI \ -Write-Host '''' \ -Write-Host ''Installing PowerShell MSI...'' \ -Write-Host ''========================================='' \ -if (-not (Test-Path $MsiPath)) { \ - Write-Error ''MSI file not found: $MsiPath'' \ - exit 1 \ -} \ -\ -$logPath = ''C:\work\install.log'' \ -Write-Host ''Running: msiexec.exe /i $MsiPath /quiet /norestart /l*v $logPath'' \ -$process = Start-Process msiexec.exe -ArgumentList ''/i'', $MsiPath, ''/quiet'', ''/norestart'', ''/l*v'', $logPath -Wait -NoNewWindow -PassThru \ -Write-Host ''MSI installation completed with exit code: $($process.ExitCode)'' \ -\ -if ($process.ExitCode -ne 0) { \ - Write-Warning ''MSI installation returned non-zero exit code. Check install.log for details.'' \ -} \ -\ -# Take post-installation snapshot \ -Write-Host '''' \ -Write-Host ''Taking post-installation snapshot...'' \ -Write-Host ''========================================='' \ -& asa collect -f -s -r -u -p -l --directories ''C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'' \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error ''Failed to take post-installation snapshot'' \ - exit 1 \ -} \ -Write-Host ''Post-installation snapshot completed'' \ -\ -# Export results \ -Write-Host '''' \ -Write-Host ''Exporting comparison results...'' \ -Write-Host ''========================================='' \ -& asa export-collect --outputsarif --savetodatabase \ -if ($LASTEXITCODE -ne 0) { \ - Write-Warning ''Export completed with exit code: $LASTEXITCODE'' \ -} \ -\ -# List and copy results to work directory \ -Write-Host '''' \ -Write-Host ''Copying results to work directory...'' \ -Write-Host ''========================================='' \ -$resultFiles = Get-ChildItem -Path . -Include ''*.txt'', ''*.sarif'', ''asa.sqlite'' -Recurse \ -foreach ($file in $resultFiles) { \ - Copy-Item -Path $file.FullName -Destination C:\work\ -Force -ErrorAction SilentlyContinue \ - Write-Host ''Copied: $($file.Name)'' \ -} \ -\ -Write-Host '''' \ -Write-Host ''========================================='' \ -Write-Host ''Attack Surface Analyzer test completed!'' \ -Write-Host ''========================================='' \ -Write-Host ''Results are available in C:\work directory'' \ -'@ | Out-File -FilePath C:\Scripts\Run-ASA-Test.ps1 -Encoding utf8 - -# Create Scripts directory -RUN New-Item -ItemType Directory -Path C:\Scripts -Force | Out-Null - -# Default command shows help -CMD ["powershell", "-NoProfile", "-Command", \ - "Write-Host 'Attack Surface Analyzer Container'; \ - Write-Host ''; \ - Write-Host 'Usage:'; \ - Write-Host ' docker run --rm --isolation process -v \":C:\\work\" powershell -File C:\\Scripts\\Run-ASA-Test.ps1 -MsiPath C:\\work\\'; \ - Write-Host ''; \ - Write-Host 'Example:'; \ - Write-Host ' docker run --rm --isolation process -v \"C:\\temp:C:\\work\" asa-test powershell -File C:\\Scripts\\Run-ASA-Test.ps1 -MsiPath C:\\work\\PowerShell.msi'; \ - Write-Host ''; \ - Write-Host 'The local path should contain the MSI file to test.'; \ - Write-Host 'Results will be written back to the same directory.'"] - -# Label for documentation -LABEL description="Windows container for running Attack Surface Analyzer tests on PowerShell MSI installations" -LABEL version="1.0" -LABEL maintainer="PowerShell Team" diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index 7b42fdb882b..3ec6ed3e80a 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -9,7 +9,7 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst ## Files - **Run-AttackSurfaceAnalyzer.ps1** - PowerShell script to run ASA tests locally -- **Dockerfile** - Dockerfile for building a container image with ASA pre-installed +- **docker/Dockerfile** - Dockerfile for building a container image with ASA pre-installed - **README.md** - This documentation file ## Prerequisites @@ -54,8 +54,8 @@ The script will: 1. Build PowerShell MSI (if not provided via -MsiPath or -NoBuild) 1. Find or use the specified MSI file 1. Create a temporary work directory -1. Start a Windows container -1. Install Attack Surface Analyzer in the container +1. Build a custom Docker container from the static Dockerfile +1. Start the Windows container with Attack Surface Analyzer 1. Take a baseline snapshot 1. Install the PowerShell MSI 1. Take a post-installation snapshot @@ -64,17 +64,16 @@ The script will: ### Option 2: Using the Dockerfile -If you prefer to build a custom image with ASA pre-installed: +If you prefer to build and use the container image directly: ```powershell -# Build the Docker image -docker build -f tools\AttackSurfaceAnalyzer\Dockerfile -t powershell-asa-test . +# Build the Docker image (Dockerfile is in docker subfolder with clean context) +docker build -f tools\AttackSurfaceAnalyzer\docker\Dockerfile -t powershell-asa-test tools\AttackSurfaceAnalyzer\docker\ -# Run the container with your MSI +# Run the container with your MSI (script is built into the container) docker run --rm --isolation process ` -v "C:\path\to\msi\directory:C:\work" ` - powershell-asa-test ` - powershell -File C:\Scripts\Run-ASA-Test.ps1 -MsiPath C:\work\PowerShell.msi + powershell-asa-test ``` ## Output Files diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 561e7bf4d9d..84d4db58a7a 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -390,134 +390,47 @@ try { Write-Log "Copying MSI to work directory..." Copy-Item $MsiPath -Destination $destMsiPath - # Create PowerShell script to run inside container - Write-Log "Creating container execution script..." - $scriptContent = @' -# Install .NET tool (ASA) -Write-Host "=========================================" -Write-Host "Installing Attack Surface Analyzer..." -Write-Host "=========================================" -dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to install Attack Surface Analyzer" - exit 1 -} -$env:PATH += ";$env:USERPROFILE\.dotnet\tools" - -# Verify ASA is available -Write-Host "" -Write-Host "Verifying ASA installation..." -& "$env:USERPROFILE\.dotnet\tools\asa.exe" --version - -# Take baseline snapshot -Write-Host "" -Write-Host "=========================================" -Write-Host "Taking baseline snapshot..." -Write-Host "=========================================" -& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to take baseline snapshot" - exit 1 -} - -# Install the MSI -Write-Host "" -Write-Host "=========================================" -Write-Host "Installing PowerShell MSI..." -Write-Host "=========================================" -$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName -Write-Host "MSI file: $msiFile" -Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow -if ($LASTEXITCODE -ne 0) { - Write-Warning "MSI installation returned exit code: $LASTEXITCODE" - Write-Host "Check install.log for details" -} - -# Take post-installation snapshot -Write-Host "" -Write-Host "=========================================" -Write-Host "Taking post-installation snapshot..." -Write-Host "=========================================" -& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to take post-installation snapshot" - exit 1 -} - -# Export results -Write-Host "" -Write-Host "=========================================" -Write-Host "Exporting comparison results..." -Write-Host "=========================================" -& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase -if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to export results with exit code: $LASTEXITCODE" -} - -# Copy results to work directory -Write-Host "" -Write-Host "=========================================" -Write-Host "Copying results to work directory..." -Write-Host "=========================================" -Get-ChildItem -Path "*.txt" | ForEach-Object { - Write-Host "Copying: $($_.Name)" - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue -} -Get-ChildItem -Path "*.sarif" | ForEach-Object { - Write-Host "Copying: $($_.Name)" - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue -} -if (Test-Path "asa.sqlite") { - Write-Host "Copying: asa.sqlite" - Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue -} - -Write-Host "" -Write-Host "=========================================" -Write-Host "Attack Surface Analyzer test completed!" -Write-Host "=========================================" -'@ - - $scriptContent | Set-Content -Path (Join-Path $containerWorkDir "run-asa.ps1") -Encoding UTF8 - - # Build Dockerfile content for reference - Write-Log "Creating Dockerfile for reference..." - $dockerfileContent = @" -# Dockerfile for Attack Surface Analyzer Testing -# This file is created for reference and can be used to build a custom image -# if you prefer not to use the inline script approach - -FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 - -# Install Attack Surface Analyzer -RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 - -# Add tools to PATH -ENV PATH="\${PATH};C:/Users/ContainerAdministrator/.dotnet/tools" - -WORKDIR C:/work - -# The container expects: -# - MSI file to be mounted to C:/work -# - Script to be mounted to C:/work/run-asa.ps1 -# Run with: docker run --rm --isolation process -v "path:C:/work" powershell -ExecutionPolicy Bypass -File C:/work/run-asa.ps1 -"@ - - $dockerfileContent | Set-Content -Path (Join-Path $containerWorkDir "Dockerfile") -Encoding UTF8 + # Use the static Dockerfile from the docker subfolder + $dockerContextPath = Join-Path $PSScriptRoot "docker" + $staticDockerfilePath = Join-Path $dockerContextPath "Dockerfile" + Write-Log "Using static Dockerfile: $staticDockerfilePath" + + if (-not (Test-Path $staticDockerfilePath)) { + Write-Log "Static Dockerfile not found at: $staticDockerfilePath" -Level ERROR + exit 1 + } + + Write-Log "Docker build context: $dockerContextPath" + # Build custom container image from static Dockerfile + Write-Log "=========================================" -Level SUCCESS + Write-Log "Building custom Attack Surface Analyzer container..." -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS + + $imageName = "powershell-asa-local:latest" + Write-Log "Building image: $imageName" + Write-Log "This may take several minutes..." + + docker build -t $imageName -f $staticDockerfilePath $dockerContextPath + + if ($LASTEXITCODE -ne 0) { + Write-Log "Docker build failed with exit code: $LASTEXITCODE" -Level ERROR + exit 1 + } + + Write-Log "Container image built successfully" -Level SUCCESS + # Run container with volume mount Write-Log "=========================================" -Level SUCCESS Write-Log "Starting Windows container..." -Level SUCCESS Write-Log "=========================================" -Level SUCCESS - Write-Log "Container image: $ContainerImage" - Write-Log "This may take several minutes..." + Write-Log "Container image: $imageName" Write-Host "" docker run --rm ` --isolation process ` -v "${containerWorkDir}:C:\work" ` - $ContainerImage ` - powershell -ExecutionPolicy Bypass -File C:\work\run-asa.ps1 + $imageName if ($LASTEXITCODE -ne 0) { Write-Log "Container execution failed with exit code: $LASTEXITCODE" -Level WARNING diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile new file mode 100644 index 00000000000..7710e240c14 --- /dev/null +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -0,0 +1,117 @@ +# Dockerfile for Attack Surface Analyzer Testing +# This builds a container image with Attack Surface Analyzer pre-installed +# and a built-in test script for testing PowerShell MSI installations + +# Use Windows Server Core with .NET SDK as base image +FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 + +# Set shell to PowerShell for easier scripting +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# Install Attack Surface Analyzer as a global .NET tool +RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 + +# Add .NET tools directory to PATH +RUN $env:PATH += ';C:/Users/ContainerAdministrator/.dotnet/tools'; \ + [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine) + +# Set working directory +WORKDIR C:/work + +# Create the ASA test script directly in the container +RUN @' \ +# Attack Surface Analyzer Test Script \ +Write-Host "=========================================" \ +Write-Host "Installing Attack Surface Analyzer..." \ +Write-Host "=========================================" \ +dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error "Failed to install Attack Surface Analyzer" \ + exit 1 \ +} \ +$env:PATH += ";$env:USERPROFILE\.dotnet\tools" \ +\ +# Verify ASA is available \ +Write-Host "" \ +Write-Host "Verifying ASA installation..." \ +& "$env:USERPROFILE\.dotnet\tools\asa.exe" --version \ +\ +# Take baseline snapshot \ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Taking baseline snapshot..." \ +Write-Host "=========================================" \ +& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error "Failed to take baseline snapshot" \ + exit 1 \ +} \ +\ +# Install the MSI \ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Installing PowerShell MSI..." \ +Write-Host "=========================================" \ +$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName \ +Write-Host "MSI file: $msiFile" \ +Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow \ +if ($LASTEXITCODE -ne 0) { \ + Write-Warning "MSI installation returned exit code: $LASTEXITCODE" \ + Write-Host "Check install.log for details" \ +} \ +\ +# Take post-installation snapshot \ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Taking post-installation snapshot..." \ +Write-Host "=========================================" \ +& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" \ +if ($LASTEXITCODE -ne 0) { \ + Write-Error "Failed to take post-installation snapshot" \ + exit 1 \ +} \ +\ +# Export results \ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Exporting comparison results..." \ +Write-Host "=========================================" \ +& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase \ +if ($LASTEXITCODE -ne 0) { \ + Write-Warning "Failed to export results with exit code: $LASTEXITCODE" \ +} \ +\ +# Copy results to work directory \ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Copying results to work directory..." \ +Write-Host "=========================================" \ +Get-ChildItem -Path "*.txt" | ForEach-Object { \ + Write-Host "Copying: $($_.Name)" \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue \ +} \ +Get-ChildItem -Path "*.sarif" | ForEach-Object { \ + Write-Host "Copying: $($_.Name)" \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue \ +} \ +if (Test-Path "asa.sqlite") { \ + Write-Host "Copying: asa.sqlite" \ + Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue \ +} \ +\ +Write-Host "" \ +Write-Host "=========================================" \ +Write-Host "Attack Surface Analyzer test completed!" \ +Write-Host "=========================================" \ +'@ | Out-File -FilePath C:\Scripts\Run-ASA-Test.ps1 -Encoding utf8 + +# Create Scripts directory +RUN New-Item -ItemType Directory -Path C:\Scripts -Force | Out-Null + +# Default entrypoint runs the ASA test script automatically +ENTRYPOINT ["powershell", "-ExecutionPolicy", "Bypass", "-File", "C:\\Scripts\\Run-ASA-Test.ps1"] + +# Label for documentation +LABEL description="Windows container for running Attack Surface Analyzer tests on PowerShell MSI installations" +LABEL version="1.0" +LABEL maintainer="PowerShell Team" From a3fd13bb8f12312bb8048a2ef5e7006cc9331498 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 18:00:36 -0800 Subject: [PATCH 24/48] Enhance README and Dockerfile documentation; implement multi-stage build for optimized result extraction in Run-AttackSurfaceAnalyzer.ps1 --- tools/AttackSurfaceAnalyzer/README.md | 41 ++++- .../Run-AttackSurfaceAnalyzer.ps1 | 94 +++++++--- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 166 ++++++++---------- 3 files changed, 179 insertions(+), 122 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index 3ec6ed3e80a..2cff26bc6da 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -9,9 +9,37 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst ## Files - **Run-AttackSurfaceAnalyzer.ps1** - PowerShell script to run ASA tests locally -- **docker/Dockerfile** - Dockerfile for building a container image with ASA pre-installed +- **docker/Dockerfile** - Multi-stage Dockerfile for building a container image with ASA pre-installed - **README.md** - This documentation file +## Docker Architecture + +The Docker implementation uses a multi-stage build to optimize the testing and result extraction process: + +### Multi-Stage Build Stages + +1. **asa-runner**: Main execution environment + - Base: `mcr.microsoft.com/dotnet/sdk:6.0-windowsservercore-ltsc2022` + - Contains Attack Surface Analyzer CLI tools + - Runs the complete test workflow + - Generates reports in both `C:\work` and `C:\reports` directories + +1. **asa-reports**: Minimal results layer + - Base: `scratch` (empty base image) + - Contains only the test reports from the runner stage + - Enables clean extraction of results without container internals + +1. **final**: Default stage (inherits from asa-runner) + - Provides backward compatibility + - Used when no specific build target is specified + +### Benefits + +- **Clean Result Extraction**: Reports are isolated in a dedicated layer +- **Efficient Transfer**: Only test results are copied, not the entire container filesystem +- **Fallback Support**: Script includes fallback to volume-based extraction if needed +- **Minimal Footprint**: Final results layer contains only the necessary output files + ## Prerequisites - Windows 10/11 or Windows Server @@ -22,6 +50,7 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst ### Build Prerequisites (if not providing -MsiPath) If you want the script to build the MSI automatically, ensure you have: + - .NET SDK (as specified in global.json) - All PowerShell build dependencies (the script will use Start-PSBuild and Start-PSPackage) - See the main PowerShell README for full build prerequisites @@ -110,19 +139,21 @@ Get-Content "*_summary.json.txt" | ConvertFrom-Json | Format-List The script automatically handles Docker Desktop installation and startup: **If Docker Desktop is installed but not running:** + - The script will automatically start Docker Desktop for you - It waits up to 60 seconds for Docker to become available - You'll be prompted for confirmation (supports `-Confirm` and `-WhatIf`) **If Docker Desktop is not installed:** + - The script will prompt you to install it automatically using winget - After installation completes, start Docker Desktop and run the script again **Manual Installation:** 1. Install Docker Desktop from https://www.docker.com/products/docker-desktop -2. Ensure Docker is running -3. Switch to Windows containers (right-click Docker tray icon → "Switch to Windows containers") +1. Ensure Docker is running +1. Switch to Windows containers (right-click Docker tray icon → "Switch to Windows containers") ### Container Fails to Start @@ -166,8 +197,8 @@ To debug issues, keep the work directory and examine the files: These tools were extracted from the GitHub Actions workflow to allow local testing. If you need to integrate ASA testing back into a CI/CD pipeline, you can: 1. Use the PowerShell script directly in your pipeline -2. Build and push the Docker image to a registry -3. Use the Dockerfile as a base for custom testing scenarios +1. Build and push the Docker image to a registry +1. Use the Dockerfile as a base for custom testing scenarios ## More Information diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 84d4db58a7a..49e4dcf814c 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -427,7 +427,9 @@ try { Write-Log "Container image: $imageName" Write-Host "" - docker run --rm ` + $containerName = "asa-test-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + + docker run --name $containerName ` --isolation process ` -v "${containerWorkDir}:C:\work" ` $imageName @@ -436,36 +438,86 @@ try { Write-Log "Container execution failed with exit code: $LASTEXITCODE" -Level WARNING } - # Copy results to output directory + # Copy results using docker cp from the reports directory in container Write-Host "" Write-Log "=========================================" -Level SUCCESS - Write-Log "Copying results to output directory..." -Level SUCCESS + Write-Log "Extracting results from container..." -Level SUCCESS Write-Log "=========================================" -Level SUCCESS - $resultFiles = @( - "*_summary.json.txt", - "*_results.json.txt", - "*.sarif", - "asa.sqlite", - "install.log" - ) - - $copiedCount = 0 - foreach ($pattern in $resultFiles) { - $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue - foreach ($file in $files) { - $destPath = Join-Path $OutputPath $file.Name - Copy-Item -Path $file.FullName -Destination $destPath -Force - Write-Log "Copied: $($file.Name) -> $destPath" -Level SUCCESS - $copiedCount++ + try { + # Copy all files from container's reports directory to output + docker cp "${containerName}:C:/reports/." $OutputPath + + if ($LASTEXITCODE -eq 0) { + $resultFiles = Get-ChildItem -Path $OutputPath -ErrorAction SilentlyContinue + $copiedCount = $resultFiles.Count + + if ($copiedCount -eq 0) { + Write-Log "Warning: No result files found in container reports" -Level WARNING + + # Fallback: try to copy from work directory + Write-Log "Attempting fallback copy from work directory..." -Level WARNING + $fallbackFiles = @( + "*_summary.json.txt", + "*_results.json.txt", + "*.sarif", + "asa.sqlite", + "install.log" + ) + + $fallbackCount = 0 + foreach ($pattern in $fallbackFiles) { + $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue + foreach ($file in $files) { + $destPath = Join-Path $OutputPath $file.Name + Copy-Item -Path $file.FullName -Destination $destPath -Force + Write-Log "Fallback copied: $($file.Name)" -Level SUCCESS + $fallbackCount++ + } + } + $copiedCount = $fallbackCount + } + else { + Write-Log "Successfully extracted $copiedCount file(s) from container:" -Level SUCCESS + $resultFiles | ForEach-Object { + Write-Log " - $($_.Name) ($([math]::Round($_.Length/1KB, 2)) KB)" -Level SUCCESS + } + } } + else { + Write-Log "Failed to extract reports from container, attempting fallback..." -Level WARNING + # Fallback to original method + $resultFiles = @( + "*_summary.json.txt", + "*_results.json.txt", + "*.sarif", + "asa.sqlite", + "install.log" + ) + + $copiedCount = 0 + foreach ($pattern in $resultFiles) { + $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue + foreach ($file in $files) { + $destPath = Join-Path $OutputPath $file.Name + Copy-Item -Path $file.FullName -Destination $destPath -Force + Write-Log "Fallback copied: $($file.Name)" -Level SUCCESS + $copiedCount++ + } + } + } + } + finally { + # Clean up the named container + Write-Log "Cleaning up container: $containerName" + docker rm $containerName -f 2>$null } if ($copiedCount -eq 0) { - Write-Log "Warning: No result files found to copy" -Level WARNING + Write-Log "Warning: No result files found" -Level WARNING } else { - Write-Log "Copied $copiedCount result file(s)" -Level SUCCESS + Write-Log "Total files extracted: $copiedCount" -Level SUCCESS } Write-Host "" diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index 7710e240c14..7332e8ba5a6 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -1,9 +1,9 @@ -# Dockerfile for Attack Surface Analyzer Testing -# This builds a container image with Attack Surface Analyzer pre-installed -# and a built-in test script for testing PowerShell MSI installations +# Multi-stage Dockerfile for Attack Surface Analyzer Testing +# Stage 1: Build and run ASA tests +# Stage 2: Extract reports to scratch layer -# Use Windows Server Core with .NET SDK as base image -FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 +# Stage 1: Test execution environment +FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 AS asa-runner # Set shell to PowerShell for easier scripting SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] @@ -15,101 +15,75 @@ RUN dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3 RUN $env:PATH += ';C:/Users/ContainerAdministrator/.dotnet/tools'; \ [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine) -# Set working directory +# Set working directory and create reports directory WORKDIR C:/work +RUN New-Item -ItemType Directory -Path C:\reports -Force | Out-Null -# Create the ASA test script directly in the container -RUN @' \ -# Attack Surface Analyzer Test Script \ -Write-Host "=========================================" \ -Write-Host "Installing Attack Surface Analyzer..." \ -Write-Host "=========================================" \ -dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI --version 2.3.328 \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error "Failed to install Attack Surface Analyzer" \ - exit 1 \ -} \ -$env:PATH += ";$env:USERPROFILE\.dotnet\tools" \ -\ -# Verify ASA is available \ -Write-Host "" \ -Write-Host "Verifying ASA installation..." \ -& "$env:USERPROFILE\.dotnet\tools\asa.exe" --version \ -\ -# Take baseline snapshot \ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Taking baseline snapshot..." \ -Write-Host "=========================================" \ -& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error "Failed to take baseline snapshot" \ - exit 1 \ -} \ -\ -# Install the MSI \ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Installing PowerShell MSI..." \ -Write-Host "=========================================" \ -$msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName \ -Write-Host "MSI file: $msiFile" \ -Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow \ -if ($LASTEXITCODE -ne 0) { \ - Write-Warning "MSI installation returned exit code: $LASTEXITCODE" \ - Write-Host "Check install.log for details" \ -} \ -\ -# Take post-installation snapshot \ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Taking post-installation snapshot..." \ -Write-Host "=========================================" \ -& "$env:USERPROFILE\.dotnet\tools\asa.exe" collect -f -s -r -u -p -l --directories "C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell" \ -if ($LASTEXITCODE -ne 0) { \ - Write-Error "Failed to take post-installation snapshot" \ - exit 1 \ -} \ -\ -# Export results \ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Exporting comparison results..." \ -Write-Host "=========================================" \ -& "$env:USERPROFILE\.dotnet\tools\asa.exe" export-collect --outputsarif --savetodatabase \ -if ($LASTEXITCODE -ne 0) { \ - Write-Warning "Failed to export results with exit code: $LASTEXITCODE" \ -} \ -\ -# Copy results to work directory \ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Copying results to work directory..." \ -Write-Host "=========================================" \ -Get-ChildItem -Path "*.txt" | ForEach-Object { \ - Write-Host "Copying: $($_.Name)" \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue \ -} \ -Get-ChildItem -Path "*.sarif" | ForEach-Object { \ - Write-Host "Copying: $($_.Name)" \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue \ -} \ -if (Test-Path "asa.sqlite") { \ - Write-Host "Copying: asa.sqlite" \ - Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue \ -} \ -\ -Write-Host "" \ -Write-Host "=========================================" \ -Write-Host "Attack Surface Analyzer test completed!" \ -Write-Host "=========================================" \ -'@ | Out-File -FilePath C:\Scripts\Run-ASA-Test.ps1 -Encoding utf8 +# Take baseline snapshot before installation +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Taking baseline snapshot..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + asa collect -f -s -r -u -p -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'; \ + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to take baseline snapshot"; exit 1 } -# Create Scripts directory -RUN New-Item -ItemType Directory -Path C:\Scripts -Force | Out-Null +# Install PowerShell MSI +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Installing PowerShell MSI..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + $msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName; \ + Write-Host "MSI file: $msiFile"; \ + Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow; \ + if ($LASTEXITCODE -ne 0) { Write-Warning "MSI installation returned exit code: $LASTEXITCODE"; Write-Host "Check install.log for details" } -# Default entrypoint runs the ASA test script automatically -ENTRYPOINT ["powershell", "-ExecutionPolicy", "Bypass", "-File", "C:\\Scripts\\Run-ASA-Test.ps1"] +# Take post-installation snapshot +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Taking post-installation snapshot..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + asa collect -f -s -r -u -p -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'; \ + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to take post-installation snapshot"; exit 1 } + +# Export comparison results +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Exporting comparison results..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + asa export-collect --outputsarif --savetodatabase; \ + if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } + +# Copy results to both work and reports directories +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Copying results to output directories..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + Get-ChildItem -Path "*.txt" | ForEach-Object { \ + Write-Host "Copying: $($_.Name)"; \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue; \ + Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction SilentlyContinue \ + }; \ + Get-ChildItem -Path "*.sarif" | ForEach-Object { \ + Write-Host "Copying: $($_.Name)"; \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue; \ + Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction SilentlyContinue \ + }; \ + if (Test-Path "asa.sqlite") { \ + Write-Host "Copying: asa.sqlite"; \ + Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue; \ + Copy-Item -Path "asa.sqlite" -Destination C:\reports\ -ErrorAction SilentlyContinue \ + }; \ + Copy-Item -Path "C:\work\install.log" -Destination C:\reports\ -ErrorAction SilentlyContinue; \ + Write-Host "Attack Surface Analyzer test completed!" -ForegroundColor Green + +# Default command shows completion message +CMD Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Container ready. Reports available in C:\reports\" -ForegroundColor Cyan; \ + Write-Host "=========================================" + +# Stage 2: Scratch layer with only the reports +FROM scratch AS asa-reports + +# Copy reports from the runner stage to scratch +COPY --from=asa-runner C:/reports/ /reports/ + +# Stage 3: Final stage (still the runner for backward compatibility) +FROM asa-runner AS final # Label for documentation LABEL description="Windows container for running Attack Surface Analyzer tests on PowerShell MSI installations" From 14d034c02ea22cf4b2c8e6cfb9a1b1a2a5e79d19 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Mon, 3 Nov 2025 18:13:01 -0800 Subject: [PATCH 25/48] Refactor MSI handling in Docker setup: copy MSI to Docker build context and improve installation logging --- .../Run-AttackSurfaceAnalyzer.ps1 | 19 ++++++++++++------- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 15 +++++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 49e4dcf814c..6db8522d5a4 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -384,14 +384,14 @@ New-Item -ItemType Directory -Force -Path $containerWorkDir | Out-Null Write-Log "Created container work directory: $containerWorkDir" try { - # Copy MSI to container work directory - $msiFileName = Split-Path $MsiPath -Leaf - $destMsiPath = Join-Path $containerWorkDir $msiFileName - Write-Log "Copying MSI to work directory..." - Copy-Item $MsiPath -Destination $destMsiPath - # Use the static Dockerfile from the docker subfolder $dockerContextPath = Join-Path $PSScriptRoot "docker" + + # Copy MSI to Docker build context + $msiFileName = Split-Path $MsiPath -Leaf + $destMsiPath = Join-Path $dockerContextPath $msiFileName + Write-Log "Copying MSI to Docker build context..." + Copy-Item $MsiPath -Destination $destMsiPath $staticDockerfilePath = Join-Path $dockerContextPath "Dockerfile" Write-Log "Using static Dockerfile: $staticDockerfilePath" @@ -431,7 +431,6 @@ try { docker run --name $containerName ` --isolation process ` - -v "${containerWorkDir}:C:\work" ` $imageName if ($LASTEXITCODE -ne 0) { @@ -536,4 +535,10 @@ finally { else { Write-Log "Work directory preserved at: $containerWorkDir" -Level SUCCESS } + + # Always cleanup MSI file from Docker build context + if ($destMsiPath -and (Test-Path $destMsiPath)) { + Write-Log "Cleaning up MSI file from Docker context..." + Remove-Item -Path $destMsiPath -Force -ErrorAction SilentlyContinue + } } diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index 7332e8ba5a6..8d642ed7405 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -26,14 +26,21 @@ RUN Write-Host "=========================================" -ForegroundColor Gree asa collect -f -s -r -u -p -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'; \ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to take baseline snapshot"; exit 1 } +# Copy the PowerShell MSI file from build context +COPY *.msi ./powershell.msi + # Install PowerShell MSI RUN Write-Host "=========================================" -ForegroundColor Green; \ Write-Host "Installing PowerShell MSI..." -ForegroundColor Green; \ Write-Host "========================================="; \ - $msiFile = Get-ChildItem -Path C:\work -Filter *.msi | Select-Object -First 1 -ExpandProperty FullName; \ - Write-Host "MSI file: $msiFile"; \ - Start-Process msiexec.exe -ArgumentList "/i", $msiFile, "/quiet", "/norestart", "/l*v", "C:\work\install.log" -Wait -NoNewWindow; \ - if ($LASTEXITCODE -ne 0) { Write-Warning "MSI installation returned exit code: $LASTEXITCODE"; Write-Host "Check install.log for details" } + Write-Host "MSI file: C:\work\powershell.msi"; \ + $argumentList = '/i C:\work\powershell.msi /quiet /norestart /l*vx C:\work\install.log ADD_PATH=1'; \ + Write-Host "Running: msiexec $argumentList"; \ + $msiProcess = Start-Process msiexec.exe -ArgumentList $argumentList -Wait -NoNewWindow -PassThru; \ + if ($msiProcess.ExitCode -ne 0) { \ + Write-Host "MSI installation failed with exit code: $($msiProcess.ExitCode)"; \ + throw "MSI installation failed. Check install.log for details" \ + } # Take post-installation snapshot RUN Write-Host "=========================================" -ForegroundColor Green; \ From 02ac17b8a4e5c6debcf277eabd64e7a947698f48 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 10:34:57 -0800 Subject: [PATCH 26/48] Update .gitignore and README; enhance Run-AttackSurfaceAnalyzer.ps1 for output path and report extraction --- .gitignore | 3 + tools/AttackSurfaceAnalyzer/README.md | 12 +- .../Run-AttackSurfaceAnalyzer.ps1 | 179 +++++++++--------- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 13 +- 4 files changed, 104 insertions(+), 103 deletions(-) diff --git a/.gitignore b/.gitignore index ccadde27182..f115e61e22d 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ TestsResults*.xml ParallelXUnitResults.xml xUnitResults.xml +# Attack Surface Analyzer results +asa-results/ + # Resharper settings PowerShell.sln.DotSettings.user *.msp diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index 2cff26bc6da..f5f80c12fec 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -24,7 +24,7 @@ The Docker implementation uses a multi-stage build to optimize the testing and r - Runs the complete test workflow - Generates reports in both `C:\work` and `C:\reports` directories -1. **asa-reports**: Minimal results layer +1. **asa-reports**: Minimal results layer - Base: `scratch` (empty base image) - Contains only the test reports from the runner stage - Enables clean extraction of results without container internals @@ -107,13 +107,13 @@ docker run --rm --isolation process ` ## Output Files -The test will generate several output files: +The test will generate output files in the `./asa-results/` directory (or your specified `-OutputPath`): -- **`*_summary.json.txt`** - Summary of detected changes -- **`*_results.json.txt`** - Detailed results in JSON format -- **`*.sarif`** - SARIF format results (can be viewed in VS Code) -- **`asa.sqlite`** - SQLite database with full analysis data +- **`asa.sqlite`** - SQLite database with full analysis data (primary result file) - **`install.log`** - MSI installation log file +- **`*_summary.json.txt`** - Summary of detected changes (if generated) +- **`*_results.json.txt`** - Detailed results in JSON format (if generated) +- **`*.sarif`** - SARIF format results (if generated, can be viewed in VS Code) ## Analyzing Results diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 6db8522d5a4..276a3aec28a 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -16,7 +16,7 @@ Skip building the MSI and only search for existing MSI files. .PARAMETER OutputPath - Directory where results will be saved. Defaults to current directory. + Directory where results will be saved. Defaults to './asa-results' subdirectory. .PARAMETER ContainerImage Docker container image to use. Defaults to mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 @@ -57,7 +57,7 @@ param( [switch]$NoBuild, [Parameter()] - [string]$OutputPath = $PWD, + [string]$OutputPath = (Join-Path $PWD "asa-results"), [Parameter()] [string]$ContainerImage = "mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022", @@ -386,7 +386,7 @@ Write-Log "Created container work directory: $containerWorkDir" try { # Use the static Dockerfile from the docker subfolder $dockerContextPath = Join-Path $PSScriptRoot "docker" - + # Copy MSI to Docker build context $msiFileName = Split-Path $MsiPath -Leaf $destMsiPath = Join-Path $dockerContextPath $msiFileName @@ -394,129 +394,124 @@ try { Copy-Item $MsiPath -Destination $destMsiPath $staticDockerfilePath = Join-Path $dockerContextPath "Dockerfile" Write-Log "Using static Dockerfile: $staticDockerfilePath" - + if (-not (Test-Path $staticDockerfilePath)) { Write-Log "Static Dockerfile not found at: $staticDockerfilePath" -Level ERROR exit 1 } - + Write-Log "Docker build context: $dockerContextPath" # Build custom container image from static Dockerfile Write-Log "=========================================" -Level SUCCESS Write-Log "Building custom Attack Surface Analyzer container..." -Level SUCCESS Write-Log "=========================================" -Level SUCCESS - - $imageName = "powershell-asa-local:latest" - Write-Log "Building image: $imageName" + + Write-Log "=========================================" -Level SUCCESS + Write-Log "Building ASA test container..." -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS Write-Log "This may take several minutes..." - - docker build -t $imageName -f $staticDockerfilePath $dockerContextPath - + + # Build the asa-reports stage specifically + $reportsImageName = "powershell-asa-reports:latest" + docker build --target asa-reports -t $reportsImageName -f $staticDockerfilePath $dockerContextPath + if ($LASTEXITCODE -ne 0) { Write-Log "Docker build failed with exit code: $LASTEXITCODE" -Level ERROR exit 1 } - - Write-Log "Container image built successfully" -Level SUCCESS - - # Run container with volume mount + + Write-Log "Build completed successfully" -Level SUCCESS + + # Extract reports from the built image Write-Log "=========================================" -Level SUCCESS - Write-Log "Starting Windows container..." -Level SUCCESS + Write-Log "Extracting reports to: $OutputPath" -Level SUCCESS Write-Log "=========================================" -Level SUCCESS - Write-Log "Container image: $imageName" - Write-Host "" - $containerName = "asa-test-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + $tempContainerName = "asa-reports-extract-$(Get-Date -Format 'yyyyMMdd-HHmmss')" - docker run --name $containerName ` - --isolation process ` - $imageName + try { + # Create a container from the reports image (but don't run it) + docker create --name $tempContainerName $reportsImageName - if ($LASTEXITCODE -ne 0) { - Write-Log "Container execution failed with exit code: $LASTEXITCODE" -Level WARNING - } + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to create temporary container for extraction" -Level ERROR + exit 1 + } - # Copy results using docker cp from the reports directory in container - Write-Host "" - Write-Log "=========================================" -Level SUCCESS - Write-Log "Extracting results from container..." -Level SUCCESS - Write-Log "=========================================" -Level SUCCESS + # Try to extract known report file patterns individually + Write-Log "Extracting report files..." -Level INFO - try { - # Copy all files from container's reports directory to output - docker cp "${containerName}:C:/reports/." $OutputPath - - if ($LASTEXITCODE -eq 0) { - $resultFiles = Get-ChildItem -Path $OutputPath -ErrorAction SilentlyContinue - $copiedCount = $resultFiles.Count - - if ($copiedCount -eq 0) { - Write-Log "Warning: No result files found in container reports" -Level WARNING - - # Fallback: try to copy from work directory - Write-Log "Attempting fallback copy from work directory..." -Level WARNING - $fallbackFiles = @( - "*_summary.json.txt", - "*_results.json.txt", - "*.sarif", - "asa.sqlite", - "install.log" - ) - - $fallbackCount = 0 - foreach ($pattern in $fallbackFiles) { - $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue - foreach ($file in $files) { - $destPath = Join-Path $OutputPath $file.Name - Copy-Item -Path $file.FullName -Destination $destPath -Force - Write-Log "Fallback copied: $($file.Name)" -Level SUCCESS - $fallbackCount++ - } + $reportFilePatterns = @( + "asa.sqlite", + "*.txt", + "*.sarif", + "*.json", + "install.log" + ) + + $extractedAny = $false + + foreach ($pattern in $reportFilePatterns) { + try { + Write-Log "Trying to extract pattern: $pattern" -Level INFO + docker cp "${tempContainerName}:/$pattern" $OutputPath 2>$null + + if ($LASTEXITCODE -eq 0) { + Write-Log "Successfully extracted: $pattern" -Level SUCCESS + $extractedAny = $true + } else { + Write-Log "Pattern not found or failed: $pattern" -Level INFO } - $copiedCount = $fallbackCount } - else { - Write-Log "Successfully extracted $copiedCount file(s) from container:" -Level SUCCESS - $resultFiles | ForEach-Object { - Write-Log " - $($_.Name) ($([math]::Round($_.Length/1KB, 2)) KB)" -Level SUCCESS - } + catch { + Write-Log "Error extracting pattern $pattern : $_" -Level WARNING } } - else { - Write-Log "Failed to extract reports from container, attempting fallback..." -Level WARNING - # Fallback to original method - $resultFiles = @( - "*_summary.json.txt", - "*_results.json.txt", - "*.sarif", - "asa.sqlite", - "install.log" - ) - - $copiedCount = 0 - foreach ($pattern in $resultFiles) { - $files = Get-ChildItem -Path $containerWorkDir -Filter $pattern -ErrorAction SilentlyContinue - foreach ($file in $files) { - $destPath = Join-Path $OutputPath $file.Name - Copy-Item -Path $file.FullName -Destination $destPath -Force - Write-Log "Fallback copied: $($file.Name)" -Level SUCCESS - $copiedCount++ - } + + # Alternative approach: extract the entire reports directory if individual files don't work + if (-not $extractedAny) { + Write-Log "Trying to extract entire directory..." -Level INFO + docker cp "${tempContainerName}:/" "$OutputPath/reports" 2>$null + + if ($LASTEXITCODE -eq 0) { + Write-Log "Successfully extracted reports directory" -Level SUCCESS + $extractedAny = $true } } + + if ($extractedAny) { + Write-Log "Report extraction completed successfully" -Level SUCCESS + } else { + Write-Log "No reports could be extracted - this may be normal if no issues were found" -Level WARNING + } } finally { - # Clean up the named container - Write-Log "Cleaning up container: $containerName" - docker rm $containerName -f 2>$null + # Clean up the temporary container + docker rm $tempContainerName -f 2>$null } + # Check what files were extracted + Write-Host "" + Write-Log "=========================================" -Level SUCCESS + Write-Log "Checking extracted results..." -Level SUCCESS + Write-Log "=========================================" -Level SUCCESS + + $resultFiles = Get-ChildItem -Path $OutputPath -ErrorAction SilentlyContinue + $copiedCount = $resultFiles.Count + if ($copiedCount -eq 0) { - Write-Log "Warning: No result files found" -Level WARNING + Write-Log "Warning: No result files found in extracted output" -Level WARNING } else { - Write-Log "Total files extracted: $copiedCount" -Level SUCCESS + Write-Log "Successfully extracted $copiedCount file(s):" -Level SUCCESS + $resultFiles | ForEach-Object { + if ($_.PSIsContainer) { + Write-Log " - $($_.Name) (directory)" -Level SUCCESS + } else { + Write-Log " - $($_.Name) ($([math]::Round($_.Length/1KB, 2)) KB)" -Level SUCCESS + } + } } Write-Host "" @@ -535,7 +530,7 @@ finally { else { Write-Log "Work directory preserved at: $containerWorkDir" -Level SUCCESS } - + # Always cleanup MSI file from Docker build context if ($destMsiPath -and (Test-Path $destMsiPath)) { Write-Log "Cleaning up MSI file from Docker context..." diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index 8d642ed7405..a482650a407 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -83,13 +83,16 @@ CMD Write-Host "=========================================" -ForegroundColor Gree Write-Host "Container ready. Reports available in C:\reports\" -ForegroundColor Cyan; \ Write-Host "=========================================" -# Stage 2: Scratch layer with only the reports -FROM scratch AS asa-reports +# Stage 2: Reports-only layer using minimal Windows base +FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS asa-reports -# Copy reports from the runner stage to scratch -COPY --from=asa-runner C:/reports/ /reports/ +# Set working directory to root +WORKDIR / -# Stage 3: Final stage (still the runner for backward compatibility) +# Copy only the report files from the runner stage to root level +COPY --from=asa-runner C:/reports/ ./ + +# Stage 3: Final stage (defaults to the runner for backward compatibility) FROM asa-runner AS final # Label for documentation From 1054fd0edc28426bd09c25f36d171e24d740b457 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 10:41:17 -0800 Subject: [PATCH 27/48] Refactor result file copying in Dockerfile: enhance error handling and logging for TXT, SARIF, and SQLite files --- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index a482650a407..c183be347c7 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -56,26 +56,62 @@ RUN Write-Host "=========================================" -ForegroundColor Gree asa export-collect --outputsarif --savetodatabase; \ if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } -# Copy results to both work and reports directories +# Copy TXT result files to both work and reports directories RUN Write-Host "=========================================" -ForegroundColor Green; \ - Write-Host "Copying results to output directories..." -ForegroundColor Green; \ + Write-Host "Copying TXT result files..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + $txtFiles = Get-ChildItem -Path "*.txt" -ErrorAction SilentlyContinue; \ + if ($txtFiles.Count -eq 0) { \ + Write-Warning 'No TXT files found to copy' \ + } else { \ + $txtFiles | ForEach-Object { \ + Write-Host "Copying: $($_.Name)"; \ + if (-not (Test-Path $_.FullName)) { \ + throw "Required TXT file not found: $($_.FullName)" \ + }; \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction Stop; \ + Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction Stop \ + } \ + } + +# Copy SARIF result files to both work and reports directories +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Copying SARIF result files..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + $sarifFiles = Get-ChildItem -Path "*.sarif" -ErrorAction SilentlyContinue; \ + if ($sarifFiles.Count -eq 0) { \ + Write-Warning 'No SARIF files found to copy' \ + } else { \ + $sarifFiles | ForEach-Object { \ + Write-Host "Copying: $($_.Name)"; \ + if (-not (Test-Path $_.FullName)) { \ + throw "Required SARIF file not found: $($_.FullName)" \ + }; \ + Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction Stop; \ + Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction Stop \ + } \ + } + +# Copy SQLite database file if it exists +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Copying SQLite database..." -ForegroundColor Green; \ Write-Host "========================================="; \ - Get-ChildItem -Path "*.txt" | ForEach-Object { \ - Write-Host "Copying: $($_.Name)"; \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue; \ - Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction SilentlyContinue \ - }; \ - Get-ChildItem -Path "*.sarif" | ForEach-Object { \ - Write-Host "Copying: $($_.Name)"; \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction SilentlyContinue; \ - Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction SilentlyContinue \ - }; \ if (Test-Path "asa.sqlite") { \ Write-Host "Copying: asa.sqlite"; \ - Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction SilentlyContinue; \ - Copy-Item -Path "asa.sqlite" -Destination C:\reports\ -ErrorAction SilentlyContinue \ + Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction Stop; \ + Copy-Item -Path "asa.sqlite" -Destination C:\reports\ -ErrorAction Stop \ + } else { \ + Write-Warning 'SQLite database (asa.sqlite) not found - this may indicate ASA export issues' \ + } + +# Copy installation log file +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Copying installation log..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + if (-not (Test-Path "C:\work\install.log")) { \ + throw "Required installation log file not found: C:\work\install.log" \ }; \ - Copy-Item -Path "C:\work\install.log" -Destination C:\reports\ -ErrorAction SilentlyContinue; \ + Copy-Item -Path "C:\work\install.log" -Destination C:\reports\ -ErrorAction Stop; \ Write-Host "Attack Surface Analyzer test completed!" -ForegroundColor Green # Default command shows completion message From 1b521134a14d6c8bb79c921ff4b472e7e4abfe88 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 11:00:38 -0800 Subject: [PATCH 28/48] Refactor report file extraction in Run-AttackSurfaceAnalyzer.ps1 and Dockerfile: standardize SARIF file naming and improve logging for file operations --- .../Run-AttackSurfaceAnalyzer.ps1 | 22 +++++------ tools/AttackSurfaceAnalyzer/docker/Dockerfile | 38 ++++++------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 276a3aec28a..dea30ad4164 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -442,30 +442,30 @@ try { # Try to extract known report file patterns individually Write-Log "Extracting report files..." -Level INFO + # Extract standardized report files directly (no file listing needed) + Write-Log "Extracting standardized report files..." -Level INFO # Extract files with standardized names (no wildcards needed) $reportFilePatterns = @( "asa.sqlite", - "*.txt", - "*.sarif", - "*.json", + "asa-results.sarif", "install.log" ) - + $extractedAny = $false - foreach ($pattern in $reportFilePatterns) { + foreach ($filename in $reportFilePatterns) { try { - Write-Log "Trying to extract pattern: $pattern" -Level INFO - docker cp "${tempContainerName}:/$pattern" $OutputPath 2>$null - + Write-Log "Trying to extract file: $filename" -Level INFO + docker cp "${tempContainerName}:/$filename" $OutputPath 2>$null + if ($LASTEXITCODE -eq 0) { - Write-Log "Successfully extracted: $pattern" -Level SUCCESS + Write-Log "Successfully extracted: $filename" -Level SUCCESS $extractedAny = $true } else { - Write-Log "Pattern not found or failed: $pattern" -Level INFO + Write-Log "File not found: $filename" -Level INFO } } catch { - Write-Log "Error extracting pattern $pattern : $_" -Level WARNING + Write-Log "Error extracting file $filename : $_" -Level WARNING } } diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index c183be347c7..cf556c38d80 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -56,39 +56,24 @@ RUN Write-Host "=========================================" -ForegroundColor Gree asa export-collect --outputsarif --savetodatabase; \ if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } -# Copy TXT result files to both work and reports directories +# Copy and standardize SARIF result files RUN Write-Host "=========================================" -ForegroundColor Green; \ - Write-Host "Copying TXT result files..." -ForegroundColor Green; \ - Write-Host "========================================="; \ - $txtFiles = Get-ChildItem -Path "*.txt" -ErrorAction SilentlyContinue; \ - if ($txtFiles.Count -eq 0) { \ - Write-Warning 'No TXT files found to copy' \ - } else { \ - $txtFiles | ForEach-Object { \ - Write-Host "Copying: $($_.Name)"; \ - if (-not (Test-Path $_.FullName)) { \ - throw "Required TXT file not found: $($_.FullName)" \ - }; \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction Stop; \ - Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction Stop \ - } \ - } - -# Copy SARIF result files to both work and reports directories -RUN Write-Host "=========================================" -ForegroundColor Green; \ - Write-Host "Copying SARIF result files..." -ForegroundColor Green; \ + Write-Host "Processing SARIF result files..." -ForegroundColor Green; \ Write-Host "========================================="; \ $sarifFiles = Get-ChildItem -Path "*.sarif" -ErrorAction SilentlyContinue; \ if ($sarifFiles.Count -eq 0) { \ - Write-Warning 'No SARIF files found to copy' \ + Write-Warning 'No SARIF files found - ASA may not have generated results' \ } else { \ $sarifFiles | ForEach-Object { \ - Write-Host "Copying: $($_.Name)"; \ + Write-Host "Found SARIF file: $($_.Name)"; \ if (-not (Test-Path $_.FullName)) { \ - throw "Required SARIF file not found: $($_.FullName)" \ + throw "SARIF file not accessible: $($_.FullName)" \ }; \ - Copy-Item -Path $_.FullName -Destination C:\work\ -ErrorAction Stop; \ - Copy-Item -Path $_.FullName -Destination C:\reports\ -ErrorAction Stop \ + Write-Host "Copying to standard name: asa-results.sarif"; \ + Copy-Item -Path $_.FullName -Destination C:\work\asa-results.sarif -ErrorAction Stop; \ + Copy-Item -Path $_.FullName -Destination C:\reports\asa-results.sarif -ErrorAction Stop; \ + Write-Host "Original filename preserved as: $($_.Name).original"; \ + Copy-Item -Path $_.FullName -Destination "C:\reports\$($_.Name).original" -ErrorAction Stop \ } \ } @@ -98,10 +83,9 @@ RUN Write-Host "=========================================" -ForegroundColor Gree Write-Host "========================================="; \ if (Test-Path "asa.sqlite") { \ Write-Host "Copying: asa.sqlite"; \ - Copy-Item -Path "asa.sqlite" -Destination C:\work\ -ErrorAction Stop; \ Copy-Item -Path "asa.sqlite" -Destination C:\reports\ -ErrorAction Stop \ } else { \ - Write-Warning 'SQLite database (asa.sqlite) not found - this may indicate ASA export issues' \ + throw 'SQLite database (asa.sqlite) not found - this may indicate ASA export issues' \ } # Copy installation log file From a6e00bd331124a874d5a11b8bb35c3633f2855eb Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 11:13:36 -0800 Subject: [PATCH 29/48] Enhance Run-AttackSurfaceAnalyzer.ps1: add support for launching ASA GUI and VS Code integration for SARIF analysis --- .../Run-AttackSurfaceAnalyzer.ps1 | 110 +++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index dea30ad4164..267337620a0 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -449,14 +449,14 @@ try { "asa-results.sarif", "install.log" ) - + $extractedAny = $false foreach ($filename in $reportFilePatterns) { try { Write-Log "Trying to extract file: $filename" -Level INFO docker cp "${tempContainerName}:/$filename" $OutputPath 2>$null - + if ($LASTEXITCODE -eq 0) { Write-Log "Successfully extracted: $filename" -Level SUCCESS $extractedAny = $true @@ -519,6 +519,112 @@ try { Write-Log "Attack Surface Analyzer test completed!" -Level SUCCESS Write-Log "=========================================" -Level SUCCESS Write-Log "Results saved to: $OutputPath" -Level SUCCESS + + # Check for ASA GUI availability and launch interactive analysis + $dbPath = Join-Path $OutputPath "asa.sqlite" + $sarifPath = Join-Path $OutputPath "asa-results.sarif" + + if (Test-Path $dbPath) { + # Check if ASA CLI is available + $asaAvailable = $false + try { + $asaVersion = asa --version 2>$null + if ($LASTEXITCODE -eq 0) { + $asaAvailable = $true + Write-Log "Attack Surface Analyzer CLI detected: $($asaVersion.Trim())" -Level INFO + } + } + catch { + # ASA not available via PATH + } + + # Try dotnet tool global path if ASA not found in PATH + if (-not $asaAvailable) { + $globalToolsPath = "$env:USERPROFILE\.dotnet\tools\asa.exe" + if (Test-Path $globalToolsPath) { + try { + $asaVersion = & $globalToolsPath --version 2>$null + if ($LASTEXITCODE -eq 0) { + $asaAvailable = $true + Write-Log "Attack Surface Analyzer found in global tools: $($asaVersion.Trim())" -Level INFO + # Use full path for subsequent commands + $asaCommand = $globalToolsPath + } + } + catch { + # Global tools ASA not working + } + } + } else { + $asaCommand = "asa" + } + + if ($asaAvailable) { + Write-Log "Launching Attack Surface Analyzer GUI for interactive analysis..." -Level SUCCESS + try { + # Launch ASA GUI with the database file + $asaProcess = Start-Process -FilePath $asaCommand -ArgumentList "gui", "--databasefilename", "`"$dbPath`"" -PassThru -NoNewWindow:$false + + if ($asaProcess) { + Write-Log "ASA GUI launched successfully (PID: $($asaProcess.Id))" -Level SUCCESS + Write-Log "Interactive analysis interface is now available" -Level INFO + } else { + Write-Log "Failed to launch ASA GUI" -Level WARNING + } + } + catch { + Write-Log "Error launching ASA GUI: $_" -Level WARNING + Write-Log "You can manually launch the GUI with: asa gui --databasefilename `"$dbPath`"" -Level INFO + } + } else { + Write-Log "Attack Surface Analyzer CLI not found" -Level INFO + Write-Log "Install ASA globally to enable GUI analysis: dotnet tool install -g Microsoft.CST.AttackSurfaceAnalyzer.CLI" -Level INFO + Write-Log "Then launch GUI manually with: asa gui --databasefilename `"$dbPath`"" -Level INFO + } + } else { + Write-Log "Database file not found - cannot launch ASA GUI" -Level WARNING + } + + # Also check for VS Code integration for SARIF analysis + if (Test-Path $sarifPath) { + # Detect if running in VS Code + $isVSCode = $false + + if ($env:VSCODE_PID -or $env:TERM_PROGRAM -eq "vscode" -or $env:VSCODE_INJECTION -eq "1") { + $isVSCode = $true + } + + # Check if 'code' command is available + if (-not $isVSCode) { + try { + $codeVersion = & code --version 2>$null + if ($LASTEXITCODE -eq 0) { + $isVSCode = $true + } + } + catch { + # 'code' command not available + } + } + + if ($isVSCode) { + Write-Log "VS Code detected - opening SARIF file for complementary analysis..." -Level INFO + try { + & code $sarifPath + if ($LASTEXITCODE -eq 0) { + Write-Log "SARIF file opened in VS Code: $sarifPath" -Level SUCCESS + } else { + Write-Log "Failed to open SARIF file in VS Code" -Level WARNING + } + } + catch { + Write-Log "Error opening SARIF file in VS Code: $_" -Level WARNING + } + } else { + Write-Log "SARIF analysis file available at: $sarifPath" -Level INFO + Write-Log "Open this file in VS Code with the SARIF Viewer extension for detailed analysis" -Level INFO + } + } } finally { # Cleanup From 137fa277257c3721a41475bf51ff2c78eef29b32 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:15:30 -0800 Subject: [PATCH 30/48] Apply suggestion from @TravisEz13 --- .github/actions/infrastructure/path-filters/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/infrastructure/path-filters/action.yml b/.github/actions/infrastructure/path-filters/action.yml index 3ef91a280d4..656719262b2 100644 --- a/.github/actions/infrastructure/path-filters/action.yml +++ b/.github/actions/infrastructure/path-filters/action.yml @@ -107,7 +107,6 @@ runs: const packagingChanged = files.some(file => file === '.github/workflows/windows-ci.yml' || - file. === '.github/workflows/windows-packaging-reusable.yml' || file === '.github/workflows/linux-ci.yml' || file.startsWith('assets/wix/') || file === 'PowerShell.Common.props' || From e168809ec2a18689d1d059036d1cc40b9cfb38f6 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:15:56 -0800 Subject: [PATCH 31/48] Apply suggestion from @TravisEz13 --- .github/workflows/windows-packaging-reusable.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 99940d7b2c0..5d30078c259 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -24,9 +24,6 @@ jobs: - architecture: x64 channel: preview runtimePrefix: win7 - - architecture: x64 - channel: stable - runtimePrefix: win7 - architecture: x86 channel: stable runtimePrefix: win7 From 7343d3614f4c09878e7a82e69ee1159baabca744 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:17:38 -0800 Subject: [PATCH 32/48] Apply suggestion from @TravisEz13 --- .github/workflows/windows-packaging-reusable.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 5d30078c259..1f03aaf5944 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -87,4 +87,3 @@ jobs: path: | ${{ github.workspace }}/artifacts/**/* !${{ github.workspace }}/artifacts/**/*.pdb - From c86876ceb5853dbc21c232fb2056fb36597a2ed2 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:57:33 -0800 Subject: [PATCH 33/48] Update tools/AttackSurfaceAnalyzer/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/AttackSurfaceAnalyzer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index f5f80c12fec..a0dc9ddea2f 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -19,7 +19,7 @@ The Docker implementation uses a multi-stage build to optimize the testing and r ### Multi-Stage Build Stages 1. **asa-runner**: Main execution environment - - Base: `mcr.microsoft.com/dotnet/sdk:6.0-windowsservercore-ltsc2022` + - Base: `mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022` - Contains Attack Surface Analyzer CLI tools - Runs the complete test workflow - Generates reports in both `C:\work` and `C:\reports` directories From 669283fdbde746dc42c7bc234de61cd5cd99bc3d Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:57:46 -0800 Subject: [PATCH 34/48] Update tools/AttackSurfaceAnalyzer/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/AttackSurfaceAnalyzer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index a0dc9ddea2f..c6b8fb79ad5 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -25,7 +25,7 @@ The Docker implementation uses a multi-stage build to optimize the testing and r - Generates reports in both `C:\work` and `C:\reports` directories 1. **asa-reports**: Minimal results layer - - Base: `scratch` (empty base image) + - Base: `mcr.microsoft.com/windows/nanoserver:ltsc2022` - Contains only the test reports from the runner stage - Enables clean extraction of results without container internals From e80288bc83715c8b7740825433f69e0e146a401d Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:58:39 -0800 Subject: [PATCH 35/48] Update tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 267337620a0..1038cf92c5c 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -443,7 +443,8 @@ try { Write-Log "Extracting report files..." -Level INFO # Extract standardized report files directly (no file listing needed) - Write-Log "Extracting standardized report files..." -Level INFO # Extract files with standardized names (no wildcards needed) + # Extract files with standardized names (no wildcards needed) + Write-Log "Extracting standardized report files..." -Level INFO $reportFilePatterns = @( "asa.sqlite", "asa-results.sarif", From 566441059abcda47c9500b093667c2763086f79e Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:58:54 -0800 Subject: [PATCH 36/48] Update tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 1038cf92c5c..f87df0c9aba 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -308,10 +308,7 @@ if (-not $MsiPath) { Write-Log "Running: Start-PSBuild -Runtime win7-x64 -Configuration Release" Start-PSBuild -Runtime win7-x64 -Configuration Release -ErrorAction Stop - if ($LASTEXITCODE -ne 0) { - Write-Log "Build failed with exit code: $LASTEXITCODE" -Level ERROR - exit 1 - } + # (Removed redundant $LASTEXITCODE check after Start-PSBuild -ErrorAction Stop) Write-Log "Build completed successfully" -Level SUCCESS From c5cca9bae088a8fb549fed5738fd5b8a55c733a1 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 4 Nov 2025 11:59:18 -0800 Subject: [PATCH 37/48] Update tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Run-AttackSurfaceAnalyzer.ps1 | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index f87df0c9aba..ce6f9f3237f 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -303,27 +303,26 @@ if (-not $MsiPath) { exit 1 } - # Build PowerShell - Write-Log "Starting PowerShell build (this may take several minutes)..." - Write-Log "Running: Start-PSBuild -Runtime win7-x64 -Configuration Release" - Start-PSBuild -Runtime win7-x64 -Configuration Release -ErrorAction Stop - - # (Removed redundant $LASTEXITCODE check after Start-PSBuild -ErrorAction Stop) - - Write-Log "Build completed successfully" -Level SUCCESS + try { + Start-PSBuild -Runtime win7-x64 -Configuration Release -ErrorAction Stop + Write-Log "Build completed successfully" -Level SUCCESS + } + catch { + Write-Log "Build failed: $_" -Level ERROR + exit 1 + } # Package the MSI Write-Log "Creating MSI package..." Write-Log "Running: Start-PSPackage -Type msi -WindowsRuntime win7-x64" - Start-PSPackage -Type msi -WindowsRuntime win7-x64 -SkipReleaseChecks - - if ($LASTEXITCODE -ne 0) { - Write-Log "Packaging failed with exit code: $LASTEXITCODE" -Level ERROR + try { + Start-PSPackage -Type msi -WindowsRuntime win7-x64 -SkipReleaseChecks + Write-Log "MSI packaging completed successfully" -Level SUCCESS + } + catch { + Write-Log "Packaging failed: $_" -Level ERROR exit 1 } - - Write-Log "MSI packaging completed successfully" -Level SUCCESS - # Find the newly created MSI at the repo root Write-Log "Looking for MSI at repo root: $repoRoot" $msiFiles = Get-ChildItem -Path $repoRoot -Filter "*.msi" -ErrorAction SilentlyContinue | From 38cfdb759bd83f759554d85bcbcc8342f80ceb15 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 12:00:11 -0800 Subject: [PATCH 38/48] Update Run-AttackSurfaceAnalyzer.ps1 and Dockerfile to use JSON results format instead of SARIF --- .../Run-AttackSurfaceAnalyzer.ps1 | 22 ++++++------- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 32 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 267337620a0..35996fcc3a6 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -446,7 +446,7 @@ try { Write-Log "Extracting standardized report files..." -Level INFO # Extract files with standardized names (no wildcards needed) $reportFilePatterns = @( "asa.sqlite", - "asa-results.sarif", + "asa-results.json", "install.log" ) @@ -522,7 +522,7 @@ try { # Check for ASA GUI availability and launch interactive analysis $dbPath = Join-Path $OutputPath "asa.sqlite" - $sarifPath = Join-Path $OutputPath "asa-results.sarif" + $jsonPath = Join-Path $OutputPath "asa-results.json" if (Test-Path $dbPath) { # Check if ASA CLI is available @@ -585,8 +585,8 @@ try { Write-Log "Database file not found - cannot launch ASA GUI" -Level WARNING } - # Also check for VS Code integration for SARIF analysis - if (Test-Path $sarifPath) { + # Also check for VS Code integration for JSON analysis + if (Test-Path $jsonPath) { # Detect if running in VS Code $isVSCode = $false @@ -608,21 +608,21 @@ try { } if ($isVSCode) { - Write-Log "VS Code detected - opening SARIF file for complementary analysis..." -Level INFO + Write-Log "VS Code detected - opening JSON results for analysis..." -Level INFO try { - & code $sarifPath + & code $jsonPath if ($LASTEXITCODE -eq 0) { - Write-Log "SARIF file opened in VS Code: $sarifPath" -Level SUCCESS + Write-Log "JSON results file opened in VS Code: $jsonPath" -Level SUCCESS } else { - Write-Log "Failed to open SARIF file in VS Code" -Level WARNING + Write-Log "Failed to open JSON file in VS Code" -Level WARNING } } catch { - Write-Log "Error opening SARIF file in VS Code: $_" -Level WARNING + Write-Log "Error opening JSON file in VS Code: $_" -Level WARNING } } else { - Write-Log "SARIF analysis file available at: $sarifPath" -Level INFO - Write-Log "Open this file in VS Code with the SARIF Viewer extension for detailed analysis" -Level INFO + Write-Log "JSON analysis results available at: $jsonPath" -Level INFO + Write-Log "Open this file in VS Code or any JSON viewer for detailed analysis" -Level INFO } } } diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index cf556c38d80..b8d9dc69ed5 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -23,7 +23,7 @@ RUN New-Item -ItemType Directory -Path C:\reports -Force | Out-Null RUN Write-Host "=========================================" -ForegroundColor Green; \ Write-Host "Taking baseline snapshot..." -ForegroundColor Green; \ Write-Host "========================================="; \ - asa collect -f -s -r -u -p -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'; \ + asa collect -f -r -u -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell' --runid before; \ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to take baseline snapshot"; exit 1 } # Copy the PowerShell MSI file from build context @@ -46,32 +46,36 @@ RUN Write-Host "=========================================" -ForegroundColor Gree RUN Write-Host "=========================================" -ForegroundColor Green; \ Write-Host "Taking post-installation snapshot..." -ForegroundColor Green; \ Write-Host "========================================="; \ - asa collect -f -s -r -u -p -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell'; \ + asa collect -f -r -u -l --directories 'C:\Program Files\PowerShell,C:\Program Files (x86)\PowerShell' --runid after; \ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to take post-installation snapshot"; exit 1 } # Export comparison results RUN Write-Host "=========================================" -ForegroundColor Green; \ Write-Host "Exporting comparison results..." -ForegroundColor Green; \ Write-Host "========================================="; \ - asa export-collect --outputsarif --savetodatabase; \ + asa export-collect --savetodatabase --resultlevels WARNING,ERROR,FATAL --firstrunid before --secondrunid after; \ if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } -# Copy and standardize SARIF result files +# Copy and standardize JSON result files RUN Write-Host "=========================================" -ForegroundColor Green; \ - Write-Host "Processing SARIF result files..." -ForegroundColor Green; \ + Write-Host "Processing JSON result files..." -ForegroundColor Green; \ Write-Host "========================================="; \ - $sarifFiles = Get-ChildItem -Path "*.sarif" -ErrorAction SilentlyContinue; \ - if ($sarifFiles.Count -eq 0) { \ - Write-Warning 'No SARIF files found - ASA may not have generated results' \ + $jsonFiles = Get-ChildItem -Path "*.json.txt" -ErrorAction SilentlyContinue; \ + if ($jsonFiles.Count -eq 0) { \ + Write-Warning 'No JSON.TXT files found - checking for .json files...'; \ + $jsonFiles = Get-ChildItem -Path "*.json" -ErrorAction SilentlyContinue \ + }; \ + if ($jsonFiles.Count -eq 0) { \ + throw 'No JSON files found - ASA may not have generated results' \ } else { \ - $sarifFiles | ForEach-Object { \ - Write-Host "Found SARIF file: $($_.Name)"; \ + $jsonFiles | ForEach-Object { \ + Write-Host "Found JSON file: $($_.Name)"; \ if (-not (Test-Path $_.FullName)) { \ - throw "SARIF file not accessible: $($_.FullName)" \ + throw "JSON file not accessible: $($_.FullName)" \ }; \ - Write-Host "Copying to standard name: asa-results.sarif"; \ - Copy-Item -Path $_.FullName -Destination C:\work\asa-results.sarif -ErrorAction Stop; \ - Copy-Item -Path $_.FullName -Destination C:\reports\asa-results.sarif -ErrorAction Stop; \ + Write-Host "Copying to standard name: asa-results.json"; \ + Copy-Item -Path $_.FullName -Destination C:\work\asa-results.json -ErrorAction Stop; \ + Copy-Item -Path $_.FullName -Destination C:\reports\asa-results.json -ErrorAction Stop; \ Write-Host "Original filename preserved as: $($_.Name).original"; \ Copy-Item -Path $_.FullName -Destination "C:\reports\$($_.Name).original" -ErrorAction Stop \ } \ From 1a7a0c2c458b26c15090a8d39a3d81ad30a01dc2 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 12:27:29 -0800 Subject: [PATCH 39/48] Add Summarize-AsaResults.ps1 script for summarizing Attack Surface Analyzer results --- .../Summarize-AsaResults.ps1 | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 new file mode 100644 index 00000000000..3d6bceffdd6 --- /dev/null +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -0,0 +1,361 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Summarizes Attack Surface Analyzer (ASA) results from a JSON file. + +.DESCRIPTION + This script analyzes ASA JSON results and provides a comprehensive summary of security findings, + including counts by category, analysis levels, and detailed breakdowns of security issues. + +.PARAMETER Path + Path to the ASA results JSON file. Defaults to 'asa-results\asa-results.json' in the current directory. + +.PARAMETER ShowDetails + Shows detailed information about each finding category. + +.EXAMPLE + .\Summarize-AsaResults.ps1 + + Summarizes the ASA results with basic statistics. + +.EXAMPLE + .\Summarize-AsaResults.ps1 -ShowDetails + + Shows detailed breakdown of findings by category..NOTES + Author: GitHub Copilot + Version: 1.0 + Created for PowerShell ASA Analysis +#> + +[CmdletBinding()] +param( + [Parameter()] + [string]$Path = "asa-results\asa-results.json", + + [Parameter()] + [switch]$ShowDetails +) + +function Get-AsaSummary { + param( + [Parameter(Mandatory)] + [PSCustomObject]$AsaData + ) + + # Extract metadata + $metadata = $AsaData.Metadata + $results = $AsaData.Results + + # Initialize counters + $summary = @{ + Metadata = @{ + Version = $metadata.'compare-version' + OS = $metadata.'compare-os' + OSVersion = $metadata.'compare-osversion' + BaseRunId = "" + CompareRunId = "" + } + Categories = @{} + TotalFindings = 0 + AnalysisLevels = @{ + WARNING = 0 + ERROR = 0 + INFO = 0 + } + RuleTypes = @{} + FileIssuesByRule = @{} + TimeSpan = $null + } + + # Process each category + foreach ($categoryName in $results.PSObject.Properties.Name) { + $categoryData = $results.$categoryName + $categoryCount = $categoryData.Count + + $summary.Categories[$categoryName] = @{ + Count = $categoryCount + Items = @() + } + + $summary.TotalFindings += $categoryCount + + # Process items in category + foreach ($item in $categoryData) { + # Count analysis levels + if ($item.Analysis) { + $summary.AnalysisLevels[$item.Analysis]++ + } + + # Extract run IDs and calculate timespan + if ($item.BaseRunId) { + $summary.Metadata.BaseRunId = $item.BaseRunId + } + if ($item.CompareRunId) { + $summary.Metadata.CompareRunId = $item.CompareRunId + } + + # Process rules + foreach ($rule in $item.Rules) { + $ruleName = $rule.Name + if (-not $summary.RuleTypes.ContainsKey($ruleName)) { + $summary.RuleTypes[$ruleName] = @{ + Count = 0 + Description = $rule.Description + Flag = $rule.Flag + Platforms = $rule.Platforms + Categories = @{} + } + } + $summary.RuleTypes[$ruleName].Count++ + + # Track which categories this rule appears in + if (-not $summary.RuleTypes[$ruleName].Categories.ContainsKey($categoryName)) { + $summary.RuleTypes[$ruleName].Categories[$categoryName] = 0 + } + $summary.RuleTypes[$ruleName].Categories[$categoryName]++ + + # For file-related categories, track file extension if available + if ($categoryName -like "*FILE*" -and $item.Identity) { + $fileExtension = [System.IO.Path]::GetExtension($item.Identity).ToLower() + if (-not $fileExtension) { $fileExtension = "(no extension)" } + + if (-not $summary.FileIssuesByRule.ContainsKey($ruleName)) { + $summary.FileIssuesByRule[$ruleName] = @{} + } + if (-not $summary.FileIssuesByRule[$ruleName].ContainsKey($fileExtension)) { + $summary.FileIssuesByRule[$ruleName][$fileExtension] = 0 + } + $summary.FileIssuesByRule[$ruleName][$fileExtension]++ + } + } + + # Store item details for detailed view + $summary.Categories[$categoryName].Items += @{ + Identity = $item.Identity + Analysis = $item.Analysis + Rules = $item.Rules + } + } + } + + # Calculate timespan if we have both run IDs + if ($summary.Metadata.BaseRunId -and $summary.Metadata.CompareRunId) { + try { + $baseTime = [DateTime]::Parse($summary.Metadata.BaseRunId) + $compareTime = [DateTime]::Parse($summary.Metadata.CompareRunId) + $summary.TimeSpan = $compareTime - $baseTime + } + catch { + $summary.TimeSpan = "Unable to calculate" + } + } + + return $summary +} + +function Write-ConsoleSummary { + param( + [Parameter(Mandatory)] + [hashtable]$Summary, + + [Parameter()] + [switch]$ShowDetails + ) + + # Header + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "Attack Surface Analyzer Results Summary" -ForegroundColor Cyan + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + + # Metadata + Write-Host "Analysis Metadata:" -ForegroundColor Yellow + Write-Host " ASA Version: $($Summary.Metadata.Version)" -ForegroundColor White + Write-Host " Operating System: $($Summary.Metadata.OS) ($($Summary.Metadata.OSVersion))" -ForegroundColor White + if ($Summary.TimeSpan -and $Summary.TimeSpan -ne "Unable to calculate") { + Write-Host " Analysis Duration: $($Summary.TimeSpan.ToString())" -ForegroundColor White + } + Write-Host "" + + # Overall Statistics + Write-Host "Overall Statistics:" -ForegroundColor Yellow + Write-Host " Total Findings: $($Summary.TotalFindings)" -ForegroundColor White + + # Analysis Levels + Write-Host " Analysis Levels:" -ForegroundColor White + foreach ($level in $Summary.AnalysisLevels.Keys | Sort-Object) { + $count = $Summary.AnalysisLevels[$level] + $color = switch ($level) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFO' { 'Green' } + default { 'White' } + } + Write-Host " $level`: $count" -ForegroundColor $color + } + Write-Host "" + + # Category Breakdown + Write-Host "Findings by Category:" -ForegroundColor Yellow + $sortedCategories = $Summary.Categories.GetEnumerator() | Sort-Object { $_.Value.Count } -Descending + + foreach ($category in $sortedCategories) { + $categoryName = $category.Key + $count = $category.Value.Count + + if ($count -gt 0) { + Write-Host " $categoryName`: $count items" -ForegroundColor Cyan + } + else { + Write-Host " $categoryName`: $count items" -ForegroundColor DarkGray + } + } + Write-Host "" + + # Rule Types Summary + Write-Host "Top Security Rules Triggered:" -ForegroundColor Yellow + $topRules = $Summary.RuleTypes.GetEnumerator() | + Sort-Object { $_.Value.Count } -Descending | + Select-Object -First 10 + + foreach ($rule in $topRules) { + $ruleName = $rule.Key + $count = $rule.Value.Count + $flag = $rule.Value.Flag + + $color = switch ($flag) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFO' { 'Green' } + default { 'White' } + } + + Write-Host " [$flag] $ruleName`: $count occurrences" -ForegroundColor $color + if ($ShowDetails) { + Write-Host " Description: $($rule.Value.Description)" -ForegroundColor DarkGray + Write-Host " Platforms: $($rule.Value.Platforms -join ', ')" -ForegroundColor DarkGray + + # Show breakdown by category for this rule + if ($rule.Value.Categories.Count -gt 0) { + Write-Host " Categories:" -ForegroundColor DarkGray + foreach ($cat in $rule.Value.Categories.GetEnumerator() | Sort-Object { $_.Value } -Descending) { + Write-Host " $($cat.Key): $($cat.Value) occurrences" -ForegroundColor Gray + } + } + } + } + + # Detailed Rule Analysis by Category + if ($ShowDetails) { + Write-Host "" + Write-Host "Detailed Rule Analysis by Category:" -ForegroundColor Yellow + + # Focus on file-related categories + $fileCategories = $Summary.Categories.GetEnumerator() | Where-Object { $_.Key -like "*FILE*" -and $_.Value.Count -gt 0 } + + foreach ($category in $fileCategories) { + $categoryName = $category.Key + Write-Host "" + Write-Host " $categoryName Rules Breakdown:" -ForegroundColor Cyan + + # Get rules that appear in this category + $categoryRules = $Summary.RuleTypes.GetEnumerator() | + Where-Object { $_.Value.Categories.ContainsKey($categoryName) } | + Sort-Object { $_.Value.Categories[$categoryName] } -Descending + + foreach ($ruleEntry in $categoryRules) { + $ruleName = $ruleEntry.Key + $count = $ruleEntry.Value.Categories[$categoryName] + $flag = $ruleEntry.Value.Flag + + $color = switch ($flag) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFO' { 'Green' } + default { 'White' } + } + + Write-Host " [$flag] $ruleName`: $count files" -ForegroundColor $color + } + } + + # Show file extension breakdown if available + if ($Summary.FileIssuesByRule.Count -gt 0) { + Write-Host "" + Write-Host "File Issues by Rule and Extension:" -ForegroundColor Yellow + + foreach ($ruleEntry in $Summary.FileIssuesByRule.GetEnumerator()) { + $ruleName = $ruleEntry.Key + Write-Host "" + Write-Host " $ruleName`:" -ForegroundColor Cyan + + $sortedExtensions = $ruleEntry.Value.GetEnumerator() | Sort-Object { $_.Value } -Descending + foreach ($extEntry in $sortedExtensions) { + $extension = $extEntry.Key + $count = $extEntry.Value + Write-Host " $extension`: $count files" -ForegroundColor White + } + } + } + } + + # Detailed Category Information + if ($ShowDetails) { + Write-Host "" + Write-Host "Detailed Category Breakdown:" -ForegroundColor Yellow + + foreach ($category in $sortedCategories | Where-Object { $_.Value.Count -gt 0 }) { + $categoryName = $category.Key + $items = $category.Value.Items + + Write-Host "" + Write-Host " $categoryName ($($items.Count) items):" -ForegroundColor Cyan + + # Group by analysis level + $groupedByAnalysis = $items | Group-Object Analysis + foreach ($group in $groupedByAnalysis) { + $level = $group.Name + $count = $group.Count + + $color = switch ($level) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFO' { 'Green' } + default { 'White' } + } + + Write-Host " $level`: $count items" -ForegroundColor $color + } + } + } + + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan +} + +# Main execution +try { + # Validate input file + if (-not (Test-Path $Path)) { + Write-Error "ASA results file not found: $Path" + exit 1 + } + + Write-Verbose "Reading ASA results from: $Path" + + # Load and parse JSON + $jsonContent = Get-Content -Path $Path -Raw -Encoding UTF8 + $asaData = $jsonContent | ConvertFrom-Json + + # Generate summary + Write-Verbose "Analyzing ASA results..." + $summary = Get-AsaSummary -AsaData $asaData + + # Output results to console + Write-ConsoleSummary -Summary $summary -ShowDetails:$ShowDetails +} +catch { + Write-Error "Error processing ASA results: $($_.Exception.Message)" + Write-Error $_.ScriptStackTrace + exit 1 +} From 385003e8d9c85bff59593e76d7ca1e3b2ea7bebd Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 12:27:39 -0800 Subject: [PATCH 40/48] Refine Run-AttackSurfaceAnalyzer.ps1 parameters and add MSI signature verification --- .../Run-AttackSurfaceAnalyzer.ps1 | 209 +++++++----------- 1 file changed, 77 insertions(+), 132 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 615054648a2..3af11ef0619 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -9,11 +9,8 @@ comparison results. .PARAMETER MsiPath - Path to the PowerShell MSI file to test. If not provided, the script will build - a new MSI using Start-PSBuild. - -.PARAMETER NoBuild - Skip building the MSI and only search for existing MSI files. + Path to the official signed PowerShell MSI file to test. This must be a released, + signed MSI from the official PowerShell releases. .PARAMETER OutputPath Directory where results will be saved. Defaults to './asa-results' subdirectory. @@ -25,37 +22,33 @@ If specified, keeps the temporary work directory after the test completes. .EXAMPLE - .\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" - -.EXAMPLE - .\Run-AttackSurfaceAnalyzer.ps1 -OutputPath "C:\results" + .\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell-7.4.0-win-x64.msi" .EXAMPLE - .\Run-AttackSurfaceAnalyzer.ps1 -NoBuild + .\Run-AttackSurfaceAnalyzer.ps1 -MsiPath ".\PowerShell-7.4.0-win-x64.msi" -OutputPath "C:\asa-results" .NOTES Requires Docker Desktop with Windows containers enabled. + Requires an official signed PowerShell MSI file from a released build. Docker Desktop Handling: - If Docker Desktop is installed but not running, the script will start it automatically - If Docker Desktop is not installed, the script will prompt to install it using winget - Waits up to 60 seconds for Docker to become available after starting - Build Behavior: - - If MsiPath is not provided and NoBuild is not specified, the script will - import build.psm1 and build a new MSI package + MSI Requirements: + - The MSI must be digitally signed by Microsoft Corporation + - The MSI must be from an official PowerShell release + - Local builds or unsigned MSIs are not supported Supports -WhatIf and -Confirm for Docker installation and startup. #> [CmdletBinding(SupportsShouldProcess)] param( - [Parameter()] + [Parameter(Mandatory)] [string]$MsiPath, - [Parameter()] - [switch]$NoBuild, - [Parameter()] [string]$OutputPath = (Join-Path $PWD "asa-results"), @@ -81,6 +74,64 @@ function Write-Log { Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color } +function Test-MsiSignature { + param( + [Parameter(Mandatory)] + [string]$MsiPath + ) + + Write-Log "Verifying MSI signature..." -Level INFO + + try { + # Get the digital signature information + $signature = Get-AuthenticodeSignature -FilePath $MsiPath + + if ($signature.Status -ne 'Valid') { + Write-Log "MSI signature is not valid. Status: $($signature.Status)" -Level ERROR + return $false + } + + # Check if signed by Microsoft Corporation + $signerCertificate = $signature.SignerCertificate + if (-not $signerCertificate) { + Write-Log "No signer certificate found" -Level ERROR + return $false + } + + $subject = $signerCertificate.Subject + Write-Log "Certificate subject: $subject" -Level INFO + + # Check for Microsoft Corporation in the subject + if ($subject -notmatch "Microsoft Corporation" -and $subject -notmatch "CN=Microsoft Corporation") { + Write-Log "MSI is not signed by Microsoft Corporation" -Level ERROR + Write-Log "Expected: Microsoft Corporation" -Level ERROR + Write-Log "Found: $subject" -Level ERROR + return $false + } + + # Check certificate validity + $validFrom = $signerCertificate.NotBefore + $validTo = $signerCertificate.NotAfter + $now = Get-Date + + if ($now -lt $validFrom -or $now -gt $validTo) { + Write-Log "Certificate is not valid for current date" -Level ERROR + Write-Log "Valid from: $validFrom to: $validTo" -Level ERROR + return $false + } + + Write-Log "MSI signature verification passed" -Level SUCCESS + Write-Log "Signed by: $($signerCertificate.Subject)" -Level SUCCESS + Write-Log "Valid from: $validFrom to: $validTo" -Level SUCCESS + + return $true + } + catch { + Write-Log "Error verifying MSI signature: $_" -Level ERROR + return $false + } +} + function Test-DockerAvailable { try { $null = docker version 2>&1 @@ -245,120 +296,7 @@ if (-not (Test-DockerAvailable)) { } } -# Find or build MSI if not provided -if (-not $MsiPath) { - if ($NoBuild) { - Write-Log "No MSI path provided, searching in artifacts directory..." - $possiblePaths = @( - "$PSScriptRoot\..\..\artifacts", - "$PSScriptRoot\..\..\" - ) - - foreach ($path in $possiblePaths) { - if (Test-Path $path) { - $msiFiles = Get-ChildItem -Path $path -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue - if ($msiFiles) { - $MsiPath = $msiFiles[0].FullName - Write-Log "Found MSI: $MsiPath" -Level SUCCESS - break - } - } - } - - if (-not $MsiPath) { - Write-Log "Could not find MSI file. Please specify -MsiPath parameter or remove -NoBuild to build a new MSI." -Level ERROR - exit 1 - } - } - else { - # Build the MSI - Write-Log "No MSI path provided, building PowerShell MSI..." -Level SUCCESS - - # Find the repository root - $repoRoot = $PSScriptRoot - while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { - $repoRoot = Split-Path $repoRoot -Parent - } - - if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "build.psm1"))) { - Write-Log "Could not find build.psm1. Please run this script from the PowerShell repository." -Level ERROR - exit 1 - } - - Write-Log "Repository root: $repoRoot" - - try { - # Import build module - Write-Log "Importing build.psm1..." - Import-Module (Join-Path $repoRoot "build.psm1") -Force - - # Import packaging module - Write-Log "Importing packaging module..." - $packagingModulePath = Join-Path $repoRoot "tools\packaging\packaging.psm1" - if (Test-Path $packagingModulePath) { - Import-Module $packagingModulePath -Force - } - else { - Write-Log "Could not find packaging.psm1 at: $packagingModulePath" -Level ERROR - exit 1 - } - - try { - Start-PSBuild -Runtime win7-x64 -Configuration Release -ErrorAction Stop - Write-Log "Build completed successfully" -Level SUCCESS - } - catch { - Write-Log "Build failed: $_" -Level ERROR - exit 1 - } - - # Package the MSI - Write-Log "Creating MSI package..." - Write-Log "Running: Start-PSPackage -Type msi -WindowsRuntime win7-x64" - try { - Start-PSPackage -Type msi -WindowsRuntime win7-x64 -SkipReleaseChecks - Write-Log "MSI packaging completed successfully" -Level SUCCESS - } - catch { - Write-Log "Packaging failed: $_" -Level ERROR - exit 1 - } - # Find the newly created MSI at the repo root - Write-Log "Looking for MSI at repo root: $repoRoot" - $msiFiles = Get-ChildItem -Path $repoRoot -Filter "*.msi" -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending - if ($msiFiles) { - $MsiPath = $msiFiles[0].FullName - Write-Log "Built MSI: $MsiPath" -Level SUCCESS - } - else { - # Also check artifacts directory as fallback - Write-Log "MSI not found at repo root, checking artifacts directory..." - $artifactsPath = Join-Path $repoRoot "artifacts" - if (Test-Path $artifactsPath) { - $msiFiles = Get-ChildItem -Path $artifactsPath -Filter "*.msi" -Recurse -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending - if ($msiFiles) { - $MsiPath = $msiFiles[0].FullName - Write-Log "Found MSI in artifacts: $MsiPath" -Level SUCCESS - } - } - } - - if (-not $MsiPath) { - Write-Log "MSI was built but could not be found in repo root or artifacts directory" -Level ERROR - exit 1 - } - } - catch { - Write-Log "Error during build: $_" -Level ERROR - Write-Log $_.ScriptStackTrace -Level ERROR - exit 1 - } - } -} - -# Verify MSI exists +# Verify MSI exists and is properly signed if (-not (Test-Path $MsiPath)) { Write-Log "MSI file not found: $MsiPath" -Level ERROR exit 1 @@ -367,6 +305,13 @@ if (-not (Test-Path $MsiPath)) { $MsiPath = Resolve-Path $MsiPath Write-Log "Using MSI: $MsiPath" +# Verify MSI signature +if (-not (Test-MsiSignature -MsiPath $MsiPath)) { + Write-Log "MSI signature verification failed. Only official signed PowerShell MSIs are supported." -Level ERROR + Write-Log "Please download an official PowerShell MSI from: https://github.com/PowerShell/PowerShell/releases" -Level ERROR + exit 1 +} + # Create output directory $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) if (-not (Test-Path $OutputPath)) { @@ -594,7 +539,7 @@ try { # Check if 'code' command is available if (-not $isVSCode) { try { - $codeVersion = & code --version 2>$null + $null = & code --version 2>$null if ($LASTEXITCODE -eq 0) { $isVSCode = $true } From 37b2ed99e41a3784310af7a778eaf103fb0c102f Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 13:46:00 -0800 Subject: [PATCH 41/48] Update README.md and Dockerfile to clarify MSI requirements and enhance result exporting --- tools/AttackSurfaceAnalyzer/README.md | 92 ++++++++++++++----- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 10 +- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/README.md b/tools/AttackSurfaceAnalyzer/README.md index c6b8fb79ad5..f57bb21f8c4 100644 --- a/tools/AttackSurfaceAnalyzer/README.md +++ b/tools/AttackSurfaceAnalyzer/README.md @@ -8,7 +8,8 @@ Attack Surface Analyzer is a Microsoft tool that helps analyze changes to a syst ## Files -- **Run-AttackSurfaceAnalyzer.ps1** - PowerShell script to run ASA tests locally +- **Run-AttackSurfaceAnalyzer.ps1** - PowerShell script to run ASA tests with official MSIs +- **Summarize-AsaResults.ps1** - PowerShell script to analyze and summarize ASA results - **docker/Dockerfile** - Multi-stage Dockerfile for building a container image with ASA pre-installed - **README.md** - This documentation file @@ -45,43 +46,42 @@ The Docker implementation uses a multi-stage build to optimize the testing and r - Windows 10/11 or Windows Server - Docker Desktop with Windows containers enabled - PowerShell 5.1 or later -- (Optional) A pre-built PowerShell MSI file to test, or the script will build one for you +- **An official signed PowerShell MSI file** from a released build -### Build Prerequisites (if not providing -MsiPath) +### MSI Requirements -If you want the script to build the MSI automatically, ensure you have: +**Important:** This tool now requires an official, digitally signed PowerShell MSI from Microsoft releases: -- .NET SDK (as specified in global.json) -- All PowerShell build dependencies (the script will use Start-PSBuild and Start-PSPackage) -- See the main PowerShell README for full build prerequisites +- **Must be signed** by Microsoft Corporation +- **Must be from an official release** (downloaded from [PowerShell Releases](https://github.com/PowerShell/PowerShell/releases)) +- **Local builds are not supported** - unsigned or development MSIs will be rejected +- The script automatically verifies the digital signature before proceeding + +**Where to get official MSIs:** + +- Download from: https://github.com/PowerShell/PowerShell/releases +- Look for files like: `PowerShell-7.x.x-win-x64.msi` ## Quick Start ### Option 1: Using the PowerShell Script (Recommended) -The simplest way to run ASA tests is using the provided PowerShell script: +The script requires an official signed PowerShell MSI file: ```powershell -# Build MSI and run ASA test automatically (default behavior) -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 - -# Run with specific MSI file (skips build) -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell.msi" +# Run ASA test with official MSI (MsiPath is required) +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath "C:\path\to\PowerShell-7.4.0-win-x64.msi" -# Search for existing MSI without building -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -NoBuild - -# Specify output directory for results -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -OutputPath "C:\results" +# Specify custom output directory for results +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath ".\PowerShell-7.4.0-win-x64.msi" -OutputPath "C:\asa-results" # Keep the temporary work directory for debugging -.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -KeepWorkDirectory +.\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 -MsiPath ".\PowerShell-7.4.0-win-x64.msi" -KeepWorkDirectory ``` The script will: -1. Build PowerShell MSI (if not provided via -MsiPath or -NoBuild) -1. Find or use the specified MSI file +1. **Verify MSI signature** - Ensures the MSI is officially signed by Microsoft Corporation 1. Create a temporary work directory 1. Build a custom Docker container from the static Dockerfile 1. Start the Windows container with Attack Surface Analyzer @@ -91,6 +91,8 @@ The script will: 1. Export comparison results 1. Copy results back to your specified output directory +**Security Note:** The script will reject any MSI that is not digitally signed by Microsoft Corporation to ensure analysis is performed only on official releases. + ### Option 2: Using the Dockerfile If you prefer to build and use the container image directly: @@ -117,6 +119,28 @@ The test will generate output files in the `./asa-results/` directory (or your s ## Analyzing Results +### Using the Summary Script (Recommended) + +Use the included summary script to get a comprehensive analysis: + +```powershell +# Basic summary of ASA results +.\tools\AttackSurfaceAnalyzer\Summarize-AsaResults.ps1 + +# Detailed analysis with rule breakdowns +.\tools\AttackSurfaceAnalyzer\Summarize-AsaResults.ps1 -ShowDetails + +# Analyze results from a specific location +.\tools\AttackSurfaceAnalyzer\Summarize-AsaResults.ps1 -Path "C:\custom\path\asa-results.json" -ShowDetails +``` + +The summary script provides: + +- **Overall statistics** - Total findings, analysis levels, category breakdowns +- **Rule analysis** - Which security rules were triggered and how often +- **File analysis** - Detailed breakdown of file-related security issues by rule type +- **Category cross-reference** - Shows which rules affect which categories + ### Using VS Code The SARIF files can be opened directly in VS Code with the SARIF Viewer extension to see a formatted view of the findings. @@ -124,8 +148,9 @@ The SARIF files can be opened directly in VS Code with the SARIF Viewer extensio ### Using PowerShell ```powershell -# Read the summary file -Get-Content "*_summary.json.txt" | ConvertFrom-Json | Format-List +# Read the JSON results directly +$results = Get-Content "asa-results\asa-results.json" | ConvertFrom-Json +$results.Results.FILE_CREATED.Count # Number of files created # Query the SQLite database (requires SQLite tools) # Example: List all file changes @@ -161,6 +186,15 @@ The script automatically handles Docker Desktop installation and startup: - Check that Windows containers are enabled in Docker settings - Try pulling the base image manually: `docker pull mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022` +### MSI Signature Verification Fails + +If you get signature verification errors: + +- **Ensure you're using an official MSI** from [PowerShell Releases](https://github.com/PowerShell/PowerShell/releases) +- **Do not use local builds** - only signed release MSIs are supported +- **Check certificate validity** - very old MSIs may have expired certificates +- **Verify file integrity** - redownload the MSI if it may be corrupted + ### No Results Generated - Check the install.log file for MSI installation errors @@ -169,12 +203,20 @@ The script automatically handles Docker Desktop installation and startup: ## Advanced Usage -### Custom Container Image +### Parameters + +The `Run-AttackSurfaceAnalyzer.ps1` script supports these parameters: + +- **`-MsiPath`** (Required) - Path to the official signed PowerShell MSI file +- **`-OutputPath`** (Optional) - Directory for results (defaults to `./asa-results`) +- **`-ContainerImage`** (Optional) - Custom container base image +- **`-KeepWorkDirectory`** (Optional) - Keep temp directory for debugging -You can specify a different container image: +Example with custom container image: ```powershell .\tools\AttackSurfaceAnalyzer\Run-AttackSurfaceAnalyzer.ps1 ` + -MsiPath ".\PowerShell-7.4.0-win-x64.msi" ` -ContainerImage "mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2022" ``` diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index b8d9dc69ed5..26befe6dea5 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -56,6 +56,14 @@ RUN Write-Host "=========================================" -ForegroundColor Gree asa export-collect --savetodatabase --resultlevels WARNING,ERROR,FATAL --firstrunid before --secondrunid after; \ if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } +# Export comparison results +RUN Write-Host "=========================================" -ForegroundColor Green; \ + Write-Host "Exporting comparison results..." -ForegroundColor Green; \ + Write-Host "========================================="; \ + asa export-collect --readfromsavedcomparisons; \ + if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to export results with exit code: $LASTEXITCODE" } + + # Copy and standardize JSON result files RUN Write-Host "=========================================" -ForegroundColor Green; \ Write-Host "Processing JSON result files..." -ForegroundColor Green; \ @@ -76,8 +84,6 @@ RUN Write-Host "=========================================" -ForegroundColor Gree Write-Host "Copying to standard name: asa-results.json"; \ Copy-Item -Path $_.FullName -Destination C:\work\asa-results.json -ErrorAction Stop; \ Copy-Item -Path $_.FullName -Destination C:\reports\asa-results.json -ErrorAction Stop; \ - Write-Host "Original filename preserved as: $($_.Name).original"; \ - Copy-Item -Path $_.FullName -Destination "C:\reports\$($_.Name).original" -ErrorAction Stop \ } \ } From 5513e6579516cd362bb55e681390742ea9949ff4 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:05:11 -0800 Subject: [PATCH 42/48] Enhance Summarize-AsaResults.ps1 to include filtering options for informational and debug events, and update examples and parameters for clarity. --- .../Summarize-AsaResults.ps1 | 192 ++++++++++++++---- 1 file changed, 149 insertions(+), 43 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 index 3d6bceffdd6..5f9791cfb16 100644 --- a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -13,15 +13,26 @@ .PARAMETER ShowDetails Shows detailed information about each finding category. +.PARAMETER IncludeInformationalEvent + Includes informational events in the analysis. By default, only WARNING and ERROR events are processed. + +.PARAMETER IncludeDebugEvent + Includes debug events in the analysis. By default, only WARNING and ERROR events are processed. + .EXAMPLE .\Summarize-AsaResults.ps1 - Summarizes the ASA results with basic statistics. + Summarizes the ASA results with basic statistics, showing only WARNING and ERROR events. .EXAMPLE .\Summarize-AsaResults.ps1 -ShowDetails - Shows detailed breakdown of findings by category..NOTES + Shows detailed breakdown of findings by category, filtering out informational and debug events. + +.EXAMPLE + .\Summarize-AsaResults.ps1 -IncludeInformationalEvent + + Includes informational events along with WARNING and ERROR events in the analysis..NOTES Author: GitHub Copilot Version: 1.0 Created for PowerShell ASA Analysis @@ -33,25 +44,37 @@ param( [string]$Path = "asa-results\asa-results.json", [Parameter()] - [switch]$ShowDetails + [switch]$ShowDetails, + + [Parameter()] + [switch]$IncludeInformationalEvent, + + [Parameter()] + [switch]$IncludeDebugEvent ) function Get-AsaSummary { param( [Parameter(Mandatory)] - [PSCustomObject]$AsaData + $AsaData, + + [Parameter()] + [switch]$IncludeInformationalEvent, + + [Parameter()] + [switch]$IncludeDebugEvent ) # Extract metadata - $metadata = $AsaData.Metadata - $results = $AsaData.Results + $metadata = $AsaData["Metadata"] + $results = $AsaData["Results"] # Initialize counters $summary = @{ Metadata = @{ - Version = $metadata.'compare-version' - OS = $metadata.'compare-os' - OSVersion = $metadata.'compare-osversion' + Version = $metadata["compare-version"] + OS = $metadata["compare-os"] + OSVersion = $metadata["compare-osversion"] BaseRunId = "" CompareRunId = "" } @@ -60,49 +83,60 @@ function Get-AsaSummary { AnalysisLevels = @{ WARNING = 0 ERROR = 0 - INFO = 0 + INFORMATION = 0 + DEBUG = 0 } RuleTypes = @{} FileIssuesByRule = @{} + FileExtensionSummary = @{} TimeSpan = $null } # Process each category - foreach ($categoryName in $results.PSObject.Properties.Name) { - $categoryData = $results.$categoryName - $categoryCount = $categoryData.Count + foreach ($categoryName in $results.Keys) { + $categoryData = $results[$categoryName] $summary.Categories[$categoryName] = @{ - Count = $categoryCount + Count = 0 Items = @() } - $summary.TotalFindings += $categoryCount - - # Process items in category + # Process items in category with filtering foreach ($item in $categoryData) { - # Count analysis levels - if ($item.Analysis) { - $summary.AnalysisLevels[$item.Analysis]++ - } + # Filter events based on analysis level + $analysisLevel = $item["Analysis"] + if ($analysisLevel) { + # Skip informational events unless explicitly included + if ($analysisLevel -eq "INFORMATION" -and -not $IncludeInformationalEvent) { + continue + } + # Skip debug events unless explicitly included + if ($analysisLevel -eq "DEBUG" -and -not $IncludeDebugEvent) { + continue + } + + $summary.AnalysisLevels[$analysisLevel]++ + } # If we reach here, the item passed the filter + $summary.Categories[$categoryName].Count++ + $summary.TotalFindings++ # Extract run IDs and calculate timespan - if ($item.BaseRunId) { - $summary.Metadata.BaseRunId = $item.BaseRunId + if ($item["BaseRunId"]) { + $summary.Metadata.BaseRunId = $item["BaseRunId"] } - if ($item.CompareRunId) { - $summary.Metadata.CompareRunId = $item.CompareRunId + if ($item["CompareRunId"]) { + $summary.Metadata.CompareRunId = $item["CompareRunId"] } # Process rules - foreach ($rule in $item.Rules) { - $ruleName = $rule.Name + foreach ($rule in $item["Rules"]) { + $ruleName = $rule["Name"] if (-not $summary.RuleTypes.ContainsKey($ruleName)) { $summary.RuleTypes[$ruleName] = @{ Count = 0 - Description = $rule.Description - Flag = $rule.Flag - Platforms = $rule.Platforms + Description = $rule["Description"] + Flag = $rule["Flag"] + Platforms = $rule["Platforms"] Categories = @{} } } @@ -115,10 +149,11 @@ function Get-AsaSummary { $summary.RuleTypes[$ruleName].Categories[$categoryName]++ # For file-related categories, track file extension if available - if ($categoryName -like "*FILE*" -and $item.Identity) { - $fileExtension = [System.IO.Path]::GetExtension($item.Identity).ToLower() + if ($categoryName -like "*FILE*" -and $item["Identity"]) { + $fileExtension = [System.IO.Path]::GetExtension($item["Identity"]).ToLower() if (-not $fileExtension) { $fileExtension = "(no extension)" } + # Track by rule and extension if (-not $summary.FileIssuesByRule.ContainsKey($ruleName)) { $summary.FileIssuesByRule[$ruleName] = @{} } @@ -126,14 +161,36 @@ function Get-AsaSummary { $summary.FileIssuesByRule[$ruleName][$fileExtension] = 0 } $summary.FileIssuesByRule[$ruleName][$fileExtension]++ + + # Track overall file extension summary + if (-not $summary.FileExtensionSummary.ContainsKey($fileExtension)) { + $summary.FileExtensionSummary[$fileExtension] = @{ + Count = 0 + Rules = @{} + Categories = @{} + } + } + $summary.FileExtensionSummary[$fileExtension].Count++ + + # Track which rules affect this extension + if (-not $summary.FileExtensionSummary[$fileExtension].Rules.ContainsKey($ruleName)) { + $summary.FileExtensionSummary[$fileExtension].Rules[$ruleName] = 0 + } + $summary.FileExtensionSummary[$fileExtension].Rules[$ruleName]++ + + # Track which categories this extension appears in + if (-not $summary.FileExtensionSummary[$fileExtension].Categories.ContainsKey($categoryName)) { + $summary.FileExtensionSummary[$fileExtension].Categories[$categoryName] = 0 + } + $summary.FileExtensionSummary[$fileExtension].Categories[$categoryName]++ } } # Store item details for detailed view $summary.Categories[$categoryName].Items += @{ - Identity = $item.Identity - Analysis = $item.Analysis - Rules = $item.Rules + Identity = $item["Identity"] + Analysis = $item["Analysis"] + Rules = $item["Rules"] } } } @@ -159,7 +216,13 @@ function Write-ConsoleSummary { [hashtable]$Summary, [Parameter()] - [switch]$ShowDetails + [switch]$ShowDetails, + + [Parameter()] + [switch]$IncludeInformationalEvent, + + [Parameter()] + [switch]$IncludeDebugEvent ) # Header @@ -181,6 +244,14 @@ function Write-ConsoleSummary { Write-Host "Overall Statistics:" -ForegroundColor Yellow Write-Host " Total Findings: $($Summary.TotalFindings)" -ForegroundColor White + # Show filtering information + $filterInfo = @() + if (-not $IncludeInformationalEvent) { $filterInfo += "INFORMATION events excluded" } + if (-not $IncludeDebugEvent) { $filterInfo += "DEBUG events excluded" } + if ($filterInfo.Count -gt 0) { + Write-Host " Filtering: $($filterInfo -join ', ')" -ForegroundColor DarkYellow + } + # Analysis Levels Write-Host " Analysis Levels:" -ForegroundColor White foreach ($level in $Summary.AnalysisLevels.Keys | Sort-Object) { @@ -188,7 +259,8 @@ function Write-ConsoleSummary { $color = switch ($level) { 'ERROR' { 'Red' } 'WARNING' { 'Yellow' } - 'INFO' { 'Green' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } default { 'White' } } Write-Host " $level`: $count" -ForegroundColor $color @@ -226,7 +298,8 @@ function Write-ConsoleSummary { $color = switch ($flag) { 'ERROR' { 'Red' } 'WARNING' { 'Yellow' } - 'INFO' { 'Green' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } default { 'White' } } @@ -245,6 +318,37 @@ function Write-ConsoleSummary { } } + # File Extension Summary + if ($Summary.FileExtensionSummary.Count -gt 0) { + Write-Host "" + Write-Host "File Extension Analysis:" -ForegroundColor Yellow + + $sortedExtensions = $Summary.FileExtensionSummary.GetEnumerator() | + Sort-Object { $_.Value.Count } -Descending | + Select-Object -First 15 + + foreach ($extEntry in $sortedExtensions) { + $extension = $extEntry.Key + $count = $extEntry.Value.Count + $displayExt = if ($extension -eq "(no extension)") { $extension } else { "*$extension" } + + Write-Host " $displayExt`: $count files" -ForegroundColor Cyan + + if ($ShowDetails) { + # Show top rules for this extension + $topRulesForExt = $extEntry.Value.Rules.GetEnumerator() | + Sort-Object { $_.Value } -Descending | + Select-Object -First 3 + + foreach ($ruleEntry in $topRulesForExt) { + $ruleName = $ruleEntry.Key + $ruleCount = $ruleEntry.Value + Write-Host " $ruleName`: $ruleCount files" -ForegroundColor Gray + } + } + } + } + # Detailed Rule Analysis by Category if ($ShowDetails) { Write-Host "" @@ -271,7 +375,8 @@ function Write-ConsoleSummary { $color = switch ($flag) { 'ERROR' { 'Red' } 'WARNING' { 'Yellow' } - 'INFO' { 'Green' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } default { 'White' } } @@ -320,7 +425,8 @@ function Write-ConsoleSummary { $color = switch ($level) { 'ERROR' { 'Red' } 'WARNING' { 'Yellow' } - 'INFO' { 'Green' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } default { 'White' } } @@ -345,14 +451,14 @@ try { # Load and parse JSON $jsonContent = Get-Content -Path $Path -Raw -Encoding UTF8 - $asaData = $jsonContent | ConvertFrom-Json + $asaData = $jsonContent | ConvertFrom-Json -AsHashtable # Generate summary Write-Verbose "Analyzing ASA results..." - $summary = Get-AsaSummary -AsaData $asaData + $summary = Get-AsaSummary -AsaData $asaData -IncludeInformationalEvent:$IncludeInformationalEvent -IncludeDebugEvent:$IncludeDebugEvent # Output results to console - Write-ConsoleSummary -Summary $summary -ShowDetails:$ShowDetails + Write-ConsoleSummary -Summary $summary -ShowDetails:$ShowDetails -IncludeInformationalEvent:$IncludeInformationalEvent -IncludeDebugEvent:$IncludeDebugEvent } catch { Write-Error "Error processing ASA results: $($_.Exception.Message)" From 1b887fbabbdabe8024443a33d7051780f9fee291 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:18:27 -0800 Subject: [PATCH 43/48] Enhance Write-ConsoleSummary function to display individual file details and limit output for file-related categories --- .../Summarize-AsaResults.ps1 | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 index 5f9791cfb16..35f45bbd80a 100644 --- a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -114,7 +114,7 @@ function Get-AsaSummary { if ($analysisLevel -eq "DEBUG" -and -not $IncludeDebugEvent) { continue } - + $summary.AnalysisLevels[$analysisLevel]++ } # If we reach here, the item passed the filter $summary.Categories[$categoryName].Count++ @@ -432,6 +432,68 @@ function Write-ConsoleSummary { Write-Host " $level`: $count items" -ForegroundColor $color } + + # Show individual file details for file-related categories + if ($categoryName -like "*FILE*" -and $items.Count -gt 0) { + Write-Host "" + Write-Host " Files:" -ForegroundColor DarkCyan + + # Limit display to first 50 items to avoid overwhelming output + $displayLimit = [Math]::Min(50, $items.Count) + for ($i = 0; $i -lt $displayLimit; $i++) { + $item = $items[$i] + $identity = $item.Identity + $analysis = $item.Analysis + + $color = switch ($analysis) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } + default { 'Gray' } + } + + # Show triggered rules for this file + if ($item.Rules -and $item.Rules.Count -gt 0) { + $ruleNames = $item.Rules | ForEach-Object { $_.Name } + Write-Host " [$analysis] $identity" -ForegroundColor $color + Write-Host " Rules: $($ruleNames -join ', ')" -ForegroundColor DarkGray + } + else { + Write-Host " [$analysis] $identity" -ForegroundColor $color + } + } + + if ($items.Count -gt $displayLimit) { + Write-Host " ... and $($items.Count - $displayLimit) more files" -ForegroundColor DarkGray + } + } + # Show details for non-file categories (users, groups, etc.) + elseif ($items.Count -gt 0) { + Write-Host "" + Write-Host " Items:" -ForegroundColor DarkCyan + + $displayLimit = [Math]::Min(20, $items.Count) + for ($i = 0; $i -lt $displayLimit; $i++) { + $item = $items[$i] + $identity = $item.Identity + $analysis = $item.Analysis + + $color = switch ($analysis) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } + default { 'Gray' } + } + + Write-Host " [$analysis] $identity" -ForegroundColor $color + } + + if ($items.Count -gt $displayLimit) { + Write-Host " ... and $($items.Count - $displayLimit) more items" -ForegroundColor DarkGray + } + } } } From e1045b66df1a0032c345aae62d788a12798a7701 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:34:12 -0800 Subject: [PATCH 44/48] Enhance Write-ConsoleSummary function to display files with expired signatures, grouped by issuer, and limit output for other file-related categories. --- .../Summarize-AsaResults.ps1 | 164 +++++++++++++++--- 1 file changed, 141 insertions(+), 23 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 index 35f45bbd80a..1b3ca3f8fcb 100644 --- a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -191,6 +191,7 @@ function Get-AsaSummary { Identity = $item["Identity"] Analysis = $item["Analysis"] Rules = $item["Rules"] + Compare = $item["Compare"] } } } @@ -435,37 +436,154 @@ function Write-ConsoleSummary { # Show individual file details for file-related categories if ($categoryName -like "*FILE*" -and $items.Count -gt 0) { - Write-Host "" - Write-Host " Files:" -ForegroundColor DarkCyan + # Check if this category contains files with expired signatures + $expiredSigItems = $items | Where-Object { + $_.Rules -and ($_.Rules | Where-Object { $_.Name -eq 'Binaries with expired signatures' }) + } - # Limit display to first 50 items to avoid overwhelming output - $displayLimit = [Math]::Min(50, $items.Count) - for ($i = 0; $i -lt $displayLimit; $i++) { - $item = $items[$i] - $identity = $item.Identity - $analysis = $item.Analysis + if ($expiredSigItems.Count -gt 0) { + Write-Host "" + Write-Host " Files with Expired Signatures (grouped by Issuer):" -ForegroundColor DarkCyan + + # Group by issuer only + $groupedByIssuer = @{} + foreach ($item in $expiredSigItems) { + if ($item.Compare -and $item.Compare.SignatureStatus -and $item.Compare.SignatureStatus.SigningCertificate) { + $cert = $item.Compare.SignatureStatus.SigningCertificate + $issuer = $cert.Issuer + $notAfter = $cert.NotAfter + $identity = $item.Identity + + if (-not $groupedByIssuer.ContainsKey($issuer)) { + $groupedByIssuer[$issuer] = @() + } + $groupedByIssuer[$issuer] += [PSCustomObject]@{ + Identity = $identity + NotAfter = $notAfter + } + } + } - $color = switch ($analysis) { - 'ERROR' { 'Red' } - 'WARNING' { 'Yellow' } - 'INFORMATION' { 'Green' } - 'DEBUG' { 'DarkGray' } - default { 'Gray' } + # Display grouped results + $sortedIssuers = $groupedByIssuer.GetEnumerator() | Sort-Object Name + + foreach ($issuerGroup in $sortedIssuers) { + $issuer = $issuerGroup.Name + $files = $issuerGroup.Value + $fileCount = $files.Count + + Write-Host "" + Write-Host " Issuer: $issuer" -ForegroundColor Yellow + Write-Host " Files ($fileCount):" -ForegroundColor White + + # Sort files by expiration date (handle nulls safely) + $sortedFiles = $files | Sort-Object { + if ($_.NotAfter) { + try { [DateTime]::Parse($_.NotAfter) } + catch { [DateTime]::MaxValue } + } else { + [DateTime]::MaxValue + } + } + + # Show first 20 files per issuer + $displayLimit = [Math]::Min(20, $fileCount) + for ($i = 0; $i -lt $displayLimit; $i++) { + $file = $sortedFiles[$i] + + # Get identity - handle both hashtable and PSCustomObject + $filePath = if ($file -is [hashtable]) { $file['Identity'] } else { $file.Identity } + + # Format date without time + $expirationDate = 'Unknown' + $notAfterValue = if ($file -is [hashtable]) { $file['NotAfter'] } else { $file.NotAfter } + if ($notAfterValue) { + try { + $expirationDate = ([DateTime]::Parse($notAfterValue)).ToString('yyyy-MM-dd') + } + catch { + $expirationDate = 'Unknown' + } + } + Write-Host " [Expired: $expirationDate] $filePath" -ForegroundColor Gray + } + + if ($fileCount -gt $displayLimit) { + Write-Host " ... and $($fileCount - $displayLimit) more files" -ForegroundColor DarkGray + } } - # Show triggered rules for this file - if ($item.Rules -and $item.Rules.Count -gt 0) { - $ruleNames = $item.Rules | ForEach-Object { $_.Name } - Write-Host " [$analysis] $identity" -ForegroundColor $color - Write-Host " Rules: $($ruleNames -join ', ')" -ForegroundColor DarkGray + # Show other files (non-expired signature issues) + $otherFiles = $items | Where-Object { + -not ($_.Rules -and ($_.Rules | Where-Object { $_.Name -eq 'Binaries with expired signatures' })) } - else { - Write-Host " [$analysis] $identity" -ForegroundColor $color + + if ($otherFiles.Count -gt 0) { + Write-Host "" + Write-Host " Other Files:" -ForegroundColor DarkCyan + + $displayLimit = [Math]::Min(20, $otherFiles.Count) + for ($i = 0; $i -lt $displayLimit; $i++) { + $item = $otherFiles[$i] + $identity = $item.Identity + $analysis = $item.Analysis + + $color = switch ($analysis) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } + default { 'Gray' } + } + + if ($item.Rules -and $item.Rules.Count -gt 0) { + $ruleNames = $item.Rules | ForEach-Object { $_.Name } + Write-Host " [$analysis] $identity" -ForegroundColor $color + Write-Host " Rules: $($ruleNames -join ', ')" -ForegroundColor DarkGray + } + else { + Write-Host " [$analysis] $identity" -ForegroundColor $color + } + } + + if ($otherFiles.Count -gt $displayLimit) { + Write-Host " ... and $($otherFiles.Count - $displayLimit) more files" -ForegroundColor DarkGray + } } } + else { + # No expired signatures, show standard file listing + Write-Host "" + Write-Host " Files:" -ForegroundColor DarkCyan + + $displayLimit = [Math]::Min(50, $items.Count) + for ($i = 0; $i -lt $displayLimit; $i++) { + $item = $items[$i] + $identity = $item.Identity + $analysis = $item.Analysis + + $color = switch ($analysis) { + 'ERROR' { 'Red' } + 'WARNING' { 'Yellow' } + 'INFORMATION' { 'Green' } + 'DEBUG' { 'DarkGray' } + default { 'Gray' } + } - if ($items.Count -gt $displayLimit) { - Write-Host " ... and $($items.Count - $displayLimit) more files" -ForegroundColor DarkGray + # Show triggered rules for this file + if ($item.Rules -and $item.Rules.Count -gt 0) { + $ruleNames = $item.Rules | ForEach-Object { $_.Name } + Write-Host " [$analysis] $identity" -ForegroundColor $color + Write-Host " Rules: $($ruleNames -join ', ')" -ForegroundColor DarkGray + } + else { + Write-Host " [$analysis] $identity" -ForegroundColor $color + } + } + + if ($items.Count -gt $displayLimit) { + Write-Host " ... and $($items.Count - $displayLimit) more files" -ForegroundColor DarkGray + } } } # Show details for non-file categories (users, groups, etc.) From 18cde5f3807c5252cfda33a2322b71cb01a3697e Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:42:41 -0800 Subject: [PATCH 45/48] Refactor Write-ConsoleSummary function to sort files by full file path and display all files, removing expiration date sorting and display limits. --- .../Summarize-AsaResults.ps1 | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 index 1b3ca3f8fcb..9fcc85f9c9d 100644 --- a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -476,21 +476,11 @@ function Write-ConsoleSummary { Write-Host " Issuer: $issuer" -ForegroundColor Yellow Write-Host " Files ($fileCount):" -ForegroundColor White - # Sort files by expiration date (handle nulls safely) - $sortedFiles = $files | Sort-Object { - if ($_.NotAfter) { - try { [DateTime]::Parse($_.NotAfter) } - catch { [DateTime]::MaxValue } - } else { - [DateTime]::MaxValue - } - } - - # Show first 20 files per issuer - $displayLimit = [Math]::Min(20, $fileCount) - for ($i = 0; $i -lt $displayLimit; $i++) { - $file = $sortedFiles[$i] + # Sort files by full file path + $sortedFiles = $files | Sort-Object Identity + # Show all files + foreach ($file in $sortedFiles) { # Get identity - handle both hashtable and PSCustomObject $filePath = if ($file -is [hashtable]) { $file['Identity'] } else { $file.Identity } @@ -507,10 +497,6 @@ function Write-ConsoleSummary { } Write-Host " [Expired: $expirationDate] $filePath" -ForegroundColor Gray } - - if ($fileCount -gt $displayLimit) { - Write-Host " ... and $($fileCount - $displayLimit) more files" -ForegroundColor DarkGray - } } # Show other files (non-expired signature issues) From 637dd4a26793e1166d9ca9d493ef423f01deed7f Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:57:08 -0800 Subject: [PATCH 46/48] Add copyright notice to Run-AttackSurfaceAnalyzer.ps1 and Summarize-AsaResults.ps1 --- tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 | 3 +++ tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 index 3af11ef0619..2f7e502bff6 100644 --- a/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 +++ b/tools/AttackSurfaceAnalyzer/Run-AttackSurfaceAnalyzer.ps1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + <# .SYNOPSIS Run Attack Surface Analyzer test locally using Docker to analyze PowerShell MSI installation. diff --git a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 index 9fcc85f9c9d..00f27014037 100644 --- a/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 +++ b/tools/AttackSurfaceAnalyzer/Summarize-AsaResults.ps1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + #Requires -Version 5.1 <# .SYNOPSIS From 20a9530c9e801b0ae84dfba85ce948794af28037 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 14:58:41 -0800 Subject: [PATCH 47/48] Add copyright notice to Dockerfile --- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index 26befe6dea5..8764bd8515c 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + # Multi-stage Dockerfile for Attack Surface Analyzer Testing # Stage 1: Build and run ASA tests # Stage 2: Extract reports to scratch layer From 0e2bab77293373d77e2620e483e5b80833549fbc Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Tue, 4 Nov 2025 15:16:45 -0800 Subject: [PATCH 48/48] Update Dockerfile to specify image digests for consistency --- tools/AttackSurfaceAnalyzer/docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/AttackSurfaceAnalyzer/docker/Dockerfile b/tools/AttackSurfaceAnalyzer/docker/Dockerfile index 8764bd8515c..3e4aaa3b717 100644 --- a/tools/AttackSurfaceAnalyzer/docker/Dockerfile +++ b/tools/AttackSurfaceAnalyzer/docker/Dockerfile @@ -6,7 +6,7 @@ # Stage 2: Extract reports to scratch layer # Stage 1: Test execution environment -FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 AS asa-runner +FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022@sha256:28f3a59216a7f91dfc4730ea47e236e2ffbb519975725bf8231f57e69dab3ca8 AS asa-runner # Set shell to PowerShell for easier scripting SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] @@ -117,7 +117,7 @@ CMD Write-Host "=========================================" -ForegroundColor Gree Write-Host "=========================================" # Stage 2: Reports-only layer using minimal Windows base -FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS asa-reports +FROM mcr.microsoft.com/windows/nanoserver:ltsc2022@sha256:307874138e4dc064d0538b58c6f028419ab82fb15fcabaf6d5378ba32c235266 AS asa-reports # Set working directory to root WORKDIR /