mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
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:
co-authored by
copilot-swe-agent[bot]
TravisEz13
Copilot
parent
c8f26fec50
commit
1ba5974415
@@ -83,6 +83,9 @@ TestsResults*.xml
|
||||
ParallelXUnitResults.xml
|
||||
xUnitResults.xml
|
||||
|
||||
# Attack Surface Analyzer results
|
||||
asa-results/
|
||||
|
||||
# Resharper settings
|
||||
PowerShell.sln.DotSettings.user
|
||||
*.msp
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user