Add Attack Surface Analyzer Script (#26379)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Travis Plunk
2025-11-04 15:18:16 -08:00
committed by GitHub
co-authored by copilot-swe-agent[bot] TravisEz13 Copilot
parent c8f26fec50
commit 1ba5974415
5 changed files with 1612 additions and 0 deletions
+3
View File
@@ -83,6 +83,9 @@ TestsResults*.xml
ParallelXUnitResults.xml
xUnitResults.xml
# Attack Surface Analyzer results
asa-results/
# Resharper settings
PowerShell.sln.DotSettings.user
*.msp
+249
View File
@@ -0,0 +1,249 @@
# 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 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
## 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: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
1. **asa-reports**: Minimal results layer
- Base: `mcr.microsoft.com/windows/nanoserver:ltsc2022`
- 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
- Docker Desktop with Windows containers enabled
- PowerShell 5.1 or later
- **An official signed PowerShell MSI file** from a released build
### MSI Requirements
**Important:** This tool now requires an official, digitally signed PowerShell MSI from Microsoft releases:
- **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 script requires an official signed PowerShell MSI file:
```powershell
# 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"
# 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 -MsiPath ".\PowerShell-7.4.0-win-x64.msi" -KeepWorkDirectory
```
The script will:
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
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
**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:
```powershell
# 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 (script is built into the container)
docker run --rm --isolation process `
-v "C:\path\to\msi\directory:C:\work" `
powershell-asa-test
```
## Output Files
The test will generate output files in the `./asa-results/` directory (or your specified `-OutputPath`):
- **`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
### 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.
### Using PowerShell
```powershell
# 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
# sqlite3 asa.sqlite "SELECT * FROM file_system WHERE change_type != 'NONE'"
```
## Troubleshooting
### Docker Not Available
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
1. Ensure Docker is running
1. 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`
### 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
- Run with `-KeepWorkDirectory` to inspect the temporary work directory
- Verify the MSI file is valid and not corrupted
## Advanced Usage
### 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
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"
```
### 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
1. Build and push the Docker image to a registry
1. 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/)
@@ -0,0 +1,590 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
<#
.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 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.
.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-7.4.0-win-x64.msi"
.EXAMPLE
.\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
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(Mandatory)]
[string]$MsiPath,
[Parameter()]
[string]$OutputPath = (Join-Path $PWD "asa-results"),
[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-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
return $true
}
catch {
return $false
}
}
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
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 responding." -Level WARNING
# 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 {
# 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
}
}
}
# Verify MSI exists and is properly signed
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"
# 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)) {
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 {
# 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"
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
Write-Log "=========================================" -Level SUCCESS
Write-Log "Building ASA test container..." -Level SUCCESS
Write-Log "=========================================" -Level SUCCESS
Write-Log "This may take several minutes..."
# 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 "Build completed successfully" -Level SUCCESS
# Extract reports from the built image
Write-Log "=========================================" -Level SUCCESS
Write-Log "Extracting reports to: $OutputPath" -Level SUCCESS
Write-Log "=========================================" -Level SUCCESS
$tempContainerName = "asa-reports-extract-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
try {
# Create a container from the reports image (but don't run it)
docker create --name $tempContainerName $reportsImageName
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to create temporary container for extraction" -Level ERROR
exit 1
}
# Try to extract known report file patterns individually
Write-Log "Extracting report files..." -Level INFO
# Extract standardized report files directly (no file listing needed)
# Extract files with standardized names (no wildcards needed)
Write-Log "Extracting standardized report files..." -Level INFO
$reportFilePatterns = @(
"asa.sqlite",
"asa-results.json",
"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
} else {
Write-Log "File not found: $filename" -Level INFO
}
}
catch {
Write-Log "Error extracting file $filename : $_" -Level WARNING
}
}
# 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 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 in extracted output" -Level WARNING
}
else {
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 ""
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
# Check for ASA GUI availability and launch interactive analysis
$dbPath = Join-Path $OutputPath "asa.sqlite"
$jsonPath = Join-Path $OutputPath "asa-results.json"
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 JSON analysis
if (Test-Path $jsonPath) {
# 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 {
$null = & code --version 2>$null
if ($LASTEXITCODE -eq 0) {
$isVSCode = $true
}
}
catch {
# 'code' command not available
}
}
if ($isVSCode) {
Write-Log "VS Code detected - opening JSON results for analysis..." -Level INFO
try {
& code $jsonPath
if ($LASTEXITCODE -eq 0) {
Write-Log "JSON results file opened in VS Code: $jsonPath" -Level SUCCESS
} else {
Write-Log "Failed to open JSON file in VS Code" -Level WARNING
}
}
catch {
Write-Log "Error opening JSON file in VS Code: $_" -Level WARNING
}
} else {
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
}
}
}
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
}
# 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
}
}
@@ -0,0 +1,636 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#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.
.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, showing only WARNING and ERROR events.
.EXAMPLE
.\Summarize-AsaResults.ps1 -ShowDetails
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
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$Path = "asa-results\asa-results.json",
[Parameter()]
[switch]$ShowDetails,
[Parameter()]
[switch]$IncludeInformationalEvent,
[Parameter()]
[switch]$IncludeDebugEvent
)
function Get-AsaSummary {
param(
[Parameter(Mandatory)]
$AsaData,
[Parameter()]
[switch]$IncludeInformationalEvent,
[Parameter()]
[switch]$IncludeDebugEvent
)
# 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
INFORMATION = 0
DEBUG = 0
}
RuleTypes = @{}
FileIssuesByRule = @{}
FileExtensionSummary = @{}
TimeSpan = $null
}
# Process each category
foreach ($categoryName in $results.Keys) {
$categoryData = $results[$categoryName]
$summary.Categories[$categoryName] = @{
Count = 0
Items = @()
}
# Process items in category with filtering
foreach ($item in $categoryData) {
# 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["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)" }
# Track by rule and 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]++
# 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"]
Compare = $item["Compare"]
}
}
}
# 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,
[Parameter()]
[switch]$IncludeInformationalEvent,
[Parameter()]
[switch]$IncludeDebugEvent
)
# 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
# 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) {
$count = $Summary.AnalysisLevels[$level]
$color = switch ($level) {
'ERROR' { 'Red' }
'WARNING' { 'Yellow' }
'INFORMATION' { 'Green' }
'DEBUG' { 'DarkGray' }
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' }
'INFORMATION' { 'Green' }
'DEBUG' { 'DarkGray' }
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
}
}
}
}
# 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 ""
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' }
'INFORMATION' { 'Green' }
'DEBUG' { 'DarkGray' }
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' }
'INFORMATION' { 'Green' }
'DEBUG' { 'DarkGray' }
default { 'White' }
}
Write-Host " $level`: $count items" -ForegroundColor $color
}
# Show individual file details for file-related categories
if ($categoryName -like "*FILE*" -and $items.Count -gt 0) {
# Check if this category contains files with expired signatures
$expiredSigItems = $items | Where-Object {
$_.Rules -and ($_.Rules | Where-Object { $_.Name -eq 'Binaries with expired signatures' })
}
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
}
}
}
# 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 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 }
# 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
}
}
# Show other files (non-expired signature issues)
$otherFiles = $items | Where-Object {
-not ($_.Rules -and ($_.Rules | Where-Object { $_.Name -eq 'Binaries with expired signatures' }))
}
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' }
}
# 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
}
}
}
}
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 -AsHashtable
# Generate summary
Write-Verbose "Analyzing ASA results..."
$summary = Get-AsaSummary -AsaData $asaData -IncludeInformationalEvent:$IncludeInformationalEvent -IncludeDebugEvent:$IncludeDebugEvent
# Output results to console
Write-ConsoleSummary -Summary $summary -ShowDetails:$ShowDetails -IncludeInformationalEvent:$IncludeInformationalEvent -IncludeDebugEvent:$IncludeDebugEvent
}
catch {
Write-Error "Error processing ASA results: $($_.Exception.Message)"
Write-Error $_.ScriptStackTrace
exit 1
}
@@ -0,0 +1,134 @@
# 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
# Stage 1: Test execution environment
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';"]
# 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 and create reports directory
WORKDIR C:/work
RUN New-Item -ItemType Directory -Path C:\reports -Force | Out-Null
# Take baseline snapshot before installation
RUN Write-Host "=========================================" -ForegroundColor Green; \
Write-Host "Taking baseline snapshot..." -ForegroundColor Green; \
Write-Host "========================================="; \
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
COPY *.msi ./powershell.msi
# Install PowerShell MSI
RUN Write-Host "=========================================" -ForegroundColor Green; \
Write-Host "Installing PowerShell MSI..." -ForegroundColor Green; \
Write-Host "========================================="; \
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; \
Write-Host "Taking post-installation snapshot..." -ForegroundColor Green; \
Write-Host "========================================="; \
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 --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; \
Write-Host "========================================="; \
$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 { \
$jsonFiles | ForEach-Object { \
Write-Host "Found JSON file: $($_.Name)"; \
if (-not (Test-Path $_.FullName)) { \
throw "JSON file not accessible: $($_.FullName)" \
}; \
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; \
} \
}
# Copy SQLite database file if it exists
RUN Write-Host "=========================================" -ForegroundColor Green; \
Write-Host "Copying SQLite database..." -ForegroundColor Green; \
Write-Host "========================================="; \
if (Test-Path "asa.sqlite") { \
Write-Host "Copying: asa.sqlite"; \
Copy-Item -Path "asa.sqlite" -Destination C:\reports\ -ErrorAction Stop \
} else { \
throw '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 Stop; \
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: Reports-only layer using minimal Windows base
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022@sha256:307874138e4dc064d0538b58c6f028419ab82fb15fcabaf6d5378ba32c235266 AS asa-reports
# Set working directory to root
WORKDIR /
# 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
LABEL description="Windows container for running Attack Surface Analyzer tests on PowerShell MSI installations"
LABEL version="1.0"
LABEL maintainer="PowerShell Team"