19 |
20 | - :ram: Capture a memory image with DumpIt for Windows,
21 | - :computer: Capture a triage image with KAPE,
22 | - :closed_lock_with_key: Check for encrypted disks,
23 | - :key: Recover the active BitLocker Recovery key,
24 | - :floppy_disk: Save all artifacts, output, and audit logs to USB or source network drive.
25 |
28 |
29 | >- [MAGNET DumpIt for Windows](https://www.magnetforensics.com/resources/magnet-dumpit-for-windows/)
30 | >- [KAPE](https://www.sans.org/tools/kape)
31 | >- DumpIt.exe (64-bit) in /modules/bin
32 | >- DumpIt_arm.exe (DumpIt.exe ARM release) in /modules/bin
33 | >- (optional) DumpIt_x86.exe (DumpIt.exe x86 release) in /modules/bin
34 | >- [Encrypted Disk Detector](https://www.magnetforensics.com/resources/encrypted-disk-detector/) (EDDv310.exe) in /modules/bin/EDD
35 | >- Prior to v4, the script required specific folder configurations in place (Collections folder, Memory folder, KAPE, etc.) That’s been simplified now. Just sit `CyberPipe.ps1 `next to your KAPE directory (whether on network or USB) and the script will take care of any folder creation necessary.
36 |
39 |
40 | >- Admin permissions check before execution.
41 | >- Memory acquisition will use Magnet DumpIt for Windows (previously used Magnet RAM Capture).
42 | >- Support for x64, ARM64 and x86 architectures.
43 | >- Both memory acquistion and triage collection now facilitated via KAPE batch mode with `_kape.cli` dynamically built during execution.
44 | >- Capture directories now named to `$hostname-$timestamp` to support multiple collections from the same asset without overwriting.
45 | >- Alert if Bitlocker key not detected. Both display and (empty) text file updated if encryption key not detected.
46 | >- If key is detected it is written to the output file.
47 | >- More efficient use of variables for output files rather than relying on renaming functions during operations.
48 | >- Now just one script for Network or USB usage. Uncomment the `“Network Collection”` section for network use.
49 | >- `Stopwatch` function will calculate the total runtime of the collection.
50 | >- ASCII art `“Ceci n’est pas une pipe.”`
51 |
52 |
55 |
56 | > In the provided code, a network location of \\Server\Triage can be seen. This should be changed to reflect the specifics of your environment. Your KAPE folder will exist under this directory.
57 | >>
58 | >Permission requirements for said directory will be dependent on the nuances of the environment and what credentials are used for the script execution (interactive vs. automation).
59 | >
60 | For a walkthrough of the code visit [BakerStreetForensics](https://bakerstreetforensics.com/2023/01/16/kape-batch-mode-arm-memory-updates-to-csirt-collect-and-all-the-things-i-learned-along-the-way/).
61 |
62 | Note: this script was previously titled CSIRT-Collect. Project name and repo updated with version 4.0.
63 |
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
19 |

20 |
21 |
22 |
23 | Functions:
24 |
25 |
26 | - :ram: Capture a memory image with MAGNET DumpIt (supports x86, x64, and ARM64) or MAGNET RAM Capture for legacy systems.
27 | - :computer: Collect triage data using MAGNET Response CLI, with selectable profiles or custom options.
28 | - :closed_lock_with_key: Detect full disk encryption using MAGNET Encrypted Disk Detector.
29 | - :key: Recover BitLocker Recovery Keys from all encrypted volumes.
30 | - :floppy_disk: Store collected data, logs, and memory images to a USB device or a defined network location.
31 | - :chart_with_upwards_trend: Real-time progress monitoring during collection.
32 | - :page_facing_up: Comprehensive reporting with pre-collection volatile data and integrity hashes.
33 |
34 | Collection profiles include:
35 | - **QuickTriage** - Volatile + System Files (no RAM) - completes in ~2 minutes
36 | - **Volatile** - Only volatile data (network connections, registry, running processes)
37 | - **RAMOnly** - Memory dump only
38 | - **RAMPage** - RAM + Pagefile
39 | - **RAMSystem** - RAM + Critical System Files
40 | - **Default (Full Triage)** - RAM + Pagefile + Volatile + System Artifacts
41 |
42 |
43 | Prerequisites:
44 |
45 |
46 | >- [MAGNET Response](https://www.magnetforensics.com/resources/magnet-response/)
47 | >- [MAGNET Encrypted Disk Detector](https://www.magnetforensics.com/resources/encrypted-disk-detector/)
48 |
49 |
50 |
51 | Network Collections:
52 |
53 |
54 | CyberPipe supports saving output directly to a network share using the `-Net` parameter. Simply specify the UNC path (e.g., `\\server\share`) and the script will automatically map the network drive and perform the collection. This is ideal for automated DFIR workflows triggered by EDR or SOC alerts.
55 |
56 | ```powershell
57 | .\CyberPipe.ps1 -Net "\\server\share"
58 | ```
59 |
60 |
61 |
62 | New in 5.3:
63 |
64 |
65 |
66 | Critical PS 5.1 Exit Code Fix
67 |
68 |
69 | - **Fixed**: False failures in Windows PowerShell 5.1 after successful Magnet Response collection
70 | - **Root cause**: PS 5.1 bug where `$process.ExitCode` not reliably populated after `WaitForExit()`
71 | - **Solution**: Implemented dual validation:
72 | - Process exit code check with object refresh
73 | - File collection verification (more reliable success indicator)
74 | - Smart error handling: continues if files collected successfully despite non-zero exit code
75 |
76 |
77 | Improved Reliability
78 |
79 |
80 | - Enhanced validation logic checks for actual collected artifacts vs. relying solely on exit codes
81 | - Graceful handling of PowerShell version-specific quirks
82 | - Better error messages distinguish between genuine failures and PS 5.1 reporting issues
83 |
84 |
85 |
86 | Usage Examples:
87 |
88 |
89 | - **Run full triage (default collection profile) to local USB drive:** (RAM, Pagefile, Volatile, System Files)
90 | ```powershell
91 | .\CyberPipe.ps1
92 | ```
93 |
94 | - **Run RAM & Operating System Files (triage light) capture:**
95 | ```powershell
96 | .\CyberPipe.ps1 -CollectionProfile RAMSystem
97 | ```
98 | - **Run memory-only capture:**
99 | ```powershell
100 | .\CyberPipe.ps1 -CollectionProfile RAMOnly
101 | ```
102 |
103 |
104 | - **Run RAM & Pagefile capture:**
105 | ```powershell
106 | .\CyberPipe.ps1 -CollectionProfile RAMPage
107 | ```
108 |
109 | - **Run RAM & Operating System Files (triage light) capture:**
110 | ```powershell
111 | .\CyberPipe.ps1 -CollectionProfile RAMSystem
112 | ```
113 | - **Run volatile-only capture:**
114 | ```powershell
115 | .\CyberPipe.ps1 -CollectionProfile Volatile
116 | ```
117 |
118 | - **Run quick triage (fast collection):**
119 | ```powershell
120 | .\CyberPipe.ps1 -CollectionProfile QuickTriage
121 | ```
122 |
123 | - **Run full triage with compression:**
124 | ```powershell
125 | .\CyberPipe.ps1 -Compress
126 | ```
127 |
128 | - **Run collection to network share:**
129 | ```powershell
130 | .\CyberPipe.ps1 -Net "\\server\share"
131 | ```
132 |
133 | - **Run network collection with specific profile:**
134 | ```powershell
135 | .\CyberPipe.ps1 -Net "\\server\share" -CollectionProfile QuickTriage
136 | ```
137 |
138 | - **Run network collection with compression:**
139 | ```powershell
140 | .\CyberPipe.ps1 -Net "\\server\share" -Compress
141 | ```
142 |
143 | - _You can modify or create custom profiles by specifying CLI arguments supported by MAGNET Response._
144 |
145 |
146 | Tool Directory Structure:
147 |
148 |
149 | - **USB Collections:** The `Tools` directory should be located alongside the script:
150 | ```
151 | E:\Triage\CyberPipe\CyberPipe.ps1
152 | E:\Triage\CyberPipe\Tools\
153 | ```
154 |
155 | - **Network Collections:** The `Tools` directory should be placed in the root of the network share:
156 | ```
157 | \\Server\share\Tools\
158 | ```
159 |
160 |
161 | Prior version (KAPE support):
162 |
163 |
164 | If you previously used CyberPipe with KAPE (prior to v5), the older workflow remains available in `CyberPipe.v4.01.ps1`.
165 |
166 | > Note: CyberPipe was previously known as CSIRT-Collect. The project was renamed starting with version 4.0.
167 |
168 | For more information visit [Baker Street Forensics](https://bakerstreetforensics.com/?s=cyberpipe)
169 |
--------------------------------------------------------------------------------
/CyberPipe.v4.01.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | CyberPipe.ps1
3 | https://github.com/dwmetz/CyberPipe
4 | previously named "CSIRT-Collect"
5 | Author: @dwmetz
6 |
7 | Function: This script will:
8 | - capture a memory image with DumpIt for Windows, (x32, x64, ARM64)
9 | - capture a triage image with KAPE,
10 | - check for encrypted disks,
11 | - recover the active BitLocker Recovery key,
12 | - save all artifacts, output and audit logs to USB or source network drive.
13 |
14 | Prerequisites: (updated for v.4)
15 | - [MAGNET DumpIt for Windows](https://www.magnetforensics.com/resources/magnet-dumpit-for-windows/)
16 | - [KAPE](https://www.sans.org/tools/kape)
17 | - DumpIt.exe (64-bit) in /modules/bin
18 | - DumpIt_arm.exe (DumpIt.exe ARM release) in /modules/bin
19 | - (optional) DumpIt_x86.exe (DumpIt.exe x86 release) in /modules/bin
20 | - [Encrypted Disk Detector](https://www.magnetforensics.com/resources/encrypted-disk-detector/) (EDDv310.exe) in /modules/bin/EDD
21 | - CyberPipe.ps1 next to your KAPE directory (whether on network or USB) and the script will take care of any folder creation necessary.
22 |
23 | Execution:
24 | - Open PowerShell as Adminstrator
25 | - Execute ./CyberPipe.ps1
26 |
27 | Release Notes:
28 |
29 | v4.01 - Memory modules and EDD separated to enable easy commenting-out of memory capture for triage capture only
30 |
31 | v4.0 - "One Script to Rule them All"
32 | - Admin permissions check before execution
33 | - Memory acquisition will use Magnet DumpIt for Windows (previously used Magnet RAM Capture).
34 | - Support for x64, ARM64 and x86 architectures.
35 | - Both memory acquistion and triage collection now facilitated via KAPE batch mode with _kape.cli dynamically built during execution.
36 | - Capture directories now named to $hostname-$timestamp to support multiple collections from the same asset without overwriting.
37 | - Alert if Bitlocker key not detected. Both display and (empty) text file updated if encryption key not detected.
38 | - If key is detected it is written to the output file.
39 | - More efficient use of variables for output files rather than relying on renaming functions during operations.
40 | - Now just one script for Network or USB usage. Uncomment the “Network Collection” section for network use.
41 | - Stopwatch function will calculate the total runtime of the collection.
42 | - ASCII art “Ceci n’est pas une pipe.”
43 |
44 | #>
45 | param ([switch]$Elevated)
46 | function Test-Admin {
47 | $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
48 | $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
49 | }
50 | if ((Test-Admin) -eq $false) {
51 | if ($elevated) {
52 | } else {
53 | Write-host -fore DarkCyan "CyberPipe requires Admin permissions (not detected). Exiting."
54 | }
55 | exit
56 | }
57 | Clear-Host
58 | Write-Host ""
59 | Write-Host ""
60 | Write-Host ""
61 | Write-host -Fore Cyan "
62 | .',;::cccccc:;. ...'''''''..'.
63 | .;ccclllloooddxc. .';clooddoolcc::;:;.
64 | .:ccclllloooddxo. .,coxxxxxdl:,'..
65 | 'ccccclllooodddd' .,,'lxkxxxo:'.
66 | 'ccccclllooodddd' .,:lxOkl,;oxo,.
67 | ':cccclllooodddo. .:dkOOOOkkd;''.
68 | .:cccclllooooddo. ..;lxkOOOOOkkkd;
69 | .;ccccllloooodddc:coxkkkkOOOOOOx:.
70 | 'cccclllooooddddxxxxkkkkOOOOx:.
71 | ,ccclllooooddddxxxxxkkkxlc,.
72 | ':llllooooddddxxxxxoc;.
73 | .';:clooddddolc:,..
74 | ''''''''''
75 | "
76 | Write-Host -Fore Cyan " CyberPipe IR Collection Script v4.01"
77 | Write-Host -Fore Gray " https://github.com/dwmetz/CyberPipe"
78 | Write-Host -Fore Gray " @dwmetz | $([char]0x00A9)2023 bakerstreetforensics.com"
79 | Write-Host ""
80 | Write-Host ""
81 | $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
82 | ## Network Collection - uncomment the section below for Network use
83 | $server = "\\hydepark\automate\watchfolders\cyberpipe" # Server configuration
84 | Write-Host -Fore Gray "Mapping network drive..."
85 | $Networkpath = "Z:\"
86 | If (Test-Path -Path $Networkpath) {
87 | Write-Host -Fore Gray "Drive Exists already."
88 | }
89 | Else {
90 | # map network drive
91 | (New-Object -ComObject WScript.Network).MapNetworkDrive("Z:","$server")
92 | # check mapping again
93 | If (Test-Path -Path $Networkpath) {
94 | Write-Host -Fore Gray "Drive has been mapped."
95 | }
96 | Else {
97 | Write-Host -Fore Red "Error mapping drive."
98 | }
99 | }
100 | Set-Location Z:
101 | #>
102 | ## Below is for USB and Network:
103 | $tstamp = (Get-Date -Format "_yyyyMMddHHmm")
104 | $collection = $env:COMPUTERNAME+$tstamp
105 | $wd = Get-Location
106 | If (Test-Path -Path Collections) {
107 | Write-Host -Fore Gray "Collections directory exists."
108 | }
109 | Else {
110 | $null = mkdir Collections
111 | If (Test-Path -Path Collections) {
112 | Write-Host -Fore Gray "Collection directory created."
113 | }
114 | Else {
115 | Write-Host -For Cyan "Error creating directory."
116 | }
117 | }
118 | Set-Location Collections
119 | $CollectionHostpath = "$wd\Collections\$collection"
120 | If (Test-Path -Path $CollectionHostpath) {
121 | Write-Host -Fore Gray "Host directory already exists."
122 | }
123 | Else {
124 | $null = mkdir $CollectionHostpath
125 | If (Test-Path -Path $CollectionHostpath) {
126 | Write-Host -Fore Gray "Host directory created."
127 | }
128 | Else {
129 | Write-Host -For Cyan "Error creating directory."
130 | }
131 | }
132 | $MemoryCollectionpath = "$CollectionHostpath\Memory"
133 | If (Test-Path -Path $MemoryCollectionpath) {
134 | }
135 | Else {
136 | $null = mkdir "$CollectionHostpath\Memory"
137 | If (Test-Path -Path $MemoryCollectionpath) {
138 | }
139 | Else {
140 | Write-Host -For Red "Error creating Memory directory."
141 | }
142 | }
143 | Write-Host -Fore Gray "Determining OS build info..."
144 | [System.Environment]::OSVersion.Version > $CollectionHostpath\Memory\$env:COMPUTERNAME-profile.txt
145 | Write-Host -Fore Gray "Preparing _kape.cli..."
146 | $dest = "$CollectionHostpath"
147 | Set-Location $wd\KAPE
148 | # MEMORY COLLECTION
149 | $arm = (Get-WmiObject -Class Win32_ComputerSystem).SystemType -match '(ARM)'
150 | if ($arm -eq "True") {
151 | Write-Host "ARM detected"
152 | Set-Content -Path _kape.cli -Value "--msource C:\ --mdest $dest --module DumpIt_Memory_ARM --ul" }
153 | else {
154 | Set-Content -Path _kape.cli -Value "--msource C:\ --mdest $dest --module DumpIt_Memory --ul" }
155 | #>
156 | Add-Content -Path _kape.cli -Value "--msource C:\ --mdest $dest --module MagnetForensics_EDD --ul"
157 | Add-Content -Path _kape.cli -Value "--tsource C:\ --tdest $dest --target KapeTriage --vhdx $env:computername --zv false"
158 | Write-host -Fore Gray "Note: DumpIt, EDD & KAPE triage collection processes will launch in separate windows."
159 | Write-host -Fore Cyan "Triage aquisition will initate after memory collection completes."
160 | $null = .\kape.exe
161 | Set-Location $MemoryCollectionpath
162 | Get-ChildItem -Filter '*memdump*' -Recurse | Rename-Item -NewName {$_.name -replace 'memdump', $collection }
163 | Write-Host -Fore Gray "Checking for BitLocker Key..."
164 | (Get-BitLockerVolume -MountPoint C).KeyProtector > $CollectionHostpath\LiveResponse\$collection-key.txt
165 | If ($Null -eq (Get-Content "$CollectionHostpath\LiveResponse\$collection-key.txt")) {
166 | Write-Host -Fore yellow "Bitlocker key not identified."
167 | Set-Content -Path $CollectionHostpath\LiveResponse\$collection-key.txt -Value "No Bitlocker key identified for $env:computername"
168 | }
169 | Else {
170 | Write-Host -fore green "Bitlocker key recovered."
171 | }
172 | Set-Content -Path $CollectionHostpath\collection-complete.txt -Value "Collection complete: $((Get-Date).ToString())"
173 | Set-Location ~
174 | $StopWatch.Stop()
175 | $null = $stopwatch.Elapsed
176 | $Minutes = $StopWatch.Elapsed.Minutes
177 | $Seconds = $StopWatch.Elapsed.Seconds
178 | Write-Host -Fore Cyan "** Collection Completed in $Minutes minutes and $Seconds seconds.**"
179 |
--------------------------------------------------------------------------------
/CyberPipe.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .NOTES
3 | CyberPipe.ps1
4 | https://github.com/dwmetz/CyberPipe
5 | Formerly known as "CSIRT-Collect"
6 | Author: @dwmetz
7 |
8 | .FUNCTIONALITY
9 | This script performs the following actions:
10 | - Captures a memory image using DumpIt (Windows x86/x64/ARM64) or Magnet RAM Capture on legacy systems
11 | - Captures a triage snapshot using MAGNET Response
12 | - Checks for encrypted volumes
13 | - Recovers the active BitLocker recovery key (if available)
14 | - Saves all artifacts, logs, and outputs to USB or designated network path
15 |
16 | .SYNOPSIS
17 | CyberPipe v5.3
18 |
19 | **Prerequisites (must be present in the \Tools directory):**
20 | - [MAGNET Response](https://magnetforensics.com) — `MagnetRESPONSE.exe`
21 | - [Encrypted Disk Detector](https://www.magnetforensics.com/resources/encrypted-disk-detector/) — `EDDv310.exe`
22 | - `CyberPipe5.ps1` should be located adjacent to the `\Tools` directory (on USB or network share)
23 |
24 | **Usage:**
25 | - Launch PowerShell as Administrator
26 | - Run `.\CyberPipe.ps1`
27 |
28 | .EXAMPLE
29 | .\CyberPipe.ps1
30 | # Runs the default full triage profile with memory, pagefile, volatile data, and system files
31 |
32 | .\CyberPipe.ps1 -CollectionProfile RAMOnly
33 | # Captures only RAM and exits
34 |
35 | .\CyberPipe.ps1 -CollectionProfile Volatile
36 | # Captures only volatile data (network, registry hives, etc.)
37 |
38 | .\CyberPipe.ps1 -CollectionProfile QuickTriage
39 | # Fast triage collection (volatile + system files, no RAM) - completes in ~2 minutes
40 |
41 | .\CyberPipe.ps1 -Compress
42 | # Run full triage and compress output to ZIP file
43 |
44 | .\CyberPipe.ps1 -Net "\\server\share"
45 | # Run collection to network share instead of local USB drive
46 |
47 | .\CyberPipe.ps1 -Net "\\server\share" -CollectionProfile QuickTriage -Compress
48 | # Network collection with specific profile and compression
49 |
50 | .NOTES
51 | Virtual Environment Detection:
52 | The script automatically detects if running in a VM (VMware, Hyper-V, VirtualBox, etc.).
53 | This is important because:
54 | - VM memory dumps may not capture hypervisor-level malware
55 | - Memory overcommitment can affect collection completeness
56 | - Nested virtualization may hide attacker infrastructure
57 | - Analysts need to know environment limitations when interpreting results
58 | #>
59 | param (
60 | [switch]$Elevated,
61 | [ValidateSet("Volatile","RAMOnly","RAMPage","RAMSystem","QuickTriage","")]
62 | [string]$CollectionProfile = $env:CYBERPIPE_PROFILE,
63 | [switch]$Compress,
64 | [string]$Net = ""
65 | )
66 |
67 | function Test-Admin {
68 | $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
69 | $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
70 | }
71 | if ((Test-Admin) -eq $false) {
72 | if ($elevated) {
73 | } else {
74 | Write-host " "
75 | Write-host "CyberPipe requires Admin permissions (not detected). Exiting."
76 | Write-host " "
77 | }
78 | exit
79 | }
80 | [console]::ForegroundColor="Cyan"
81 | Clear-Host
82 |
83 | Sleep 1
84 | if ($PSVersionTable.PSEdition -eq 'Core') {
85 | $banner = @'
86 | ╔════════════════════════════════════════════════════════════════════════════╗
87 | ║ ║
88 | ║ ██████╗██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██╗██████╗ ███████╗ ║
89 | ║ ██╔════╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗██╔══██╗██║██╔══██╗██╔════╝ ║
90 | ║ ██║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝██████╔╝██║██████╔╝█████╗ ║
91 | ║ ██║ ╚██╔╝ ██╔══██╗██╔══╝ ██╔══██╗██╔═══╝ ██║██╔═══╝ ██╔══╝ ║
92 | ║ ╚██████╗ ██║ ██████╔╝███████╗██║ ██║██║ ██║██║ ███████╗ ║
93 | ║ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝ ║
94 | ║ ║
95 | ║ Incident Response Collection ║
96 | ║ v5.3 ║
97 | ║ ║
98 | ╚════════════════════════════════════════════════════════════════════════════╝
99 | Memory • Triage • Chain of Custody
100 |
101 | https://github.com/dwmetz/CyberPipe
102 |
103 | '@
104 | } else {
105 | $banner = @'
106 | +-------------------------------------------------+
107 | | CYBERPIPE |
108 | | |
109 | | Incident Response Collection |
110 | | v5.3 |
111 | | |
112 | +-------------------------------------------------+
113 | Memory - Triage - Chain of Custody
114 |
115 | https://github.com/dwmetz/CyberPipe
116 | '@
117 | }
118 |
119 | $banner
120 | $copyrightSymbol = [char]0x00A9
121 | $copyrightLine = $copyrightSymbol + '2025 @dwmetz | bakerstreetforensics.com'
122 | Write-Host ''
123 | Write-Host ''
124 | Write-Host $copyrightLine
125 | Write-Host ''
126 | Start-Sleep 1
127 |
128 | [console]::ForegroundColor='DarkCyan'
129 | ## Network Collection Handling
130 | if ($Net) {
131 | $netMsg = 'Network mode enabled. Mapping drive to {0}...' -f $Net
132 | Write-Host -Fore Cyan $netMsg
133 | $Networkpath = 'Z:\'
134 |
135 | If (Test-Path -Path $Networkpath) {
136 | Write-Host 'Drive Z: already mapped.'
137 | }
138 | Else {
139 | try {
140 | (New-Object -ComObject WScript.Network).MapNetworkDrive('Z:', $Net)
141 | Start-Sleep -Seconds 2
142 |
143 | If (Test-Path -Path $Networkpath) {
144 | $successMsg = 'Drive mapped successfully to {0}' -f $Net
145 | Write-Host -Fore Green $successMsg
146 | }
147 | Else {
148 | Write-Host -Fore Red 'Error: Drive mapping appeared to succeed but path not accessible.'
149 | exit 1
150 | }
151 | }
152 | catch {
153 | $errMsg = 'Error mapping network drive: {0}' -f $_.Exception.Message
154 | Write-Host -Fore Red $errMsg
155 | exit 1
156 | }
157 | }
158 |
159 | Set-Location $Networkpath
160 | Write-Host -Fore Cyan 'Working from network location: Z:\'
161 | }
162 | ## Below is for USB and Network:
163 | # Save the script's directory (not current location, which may change)
164 | if ($PSScriptRoot) {
165 | $wd = $PSScriptRoot
166 | } else {
167 | # Fallback for PS 2.0
168 | $wd = Split-Path -Parent $MyInvocation.MyCommand.Path
169 | }
170 | $tstamp = (Get-Date -Format 'yyyyMMddHHmm')
171 | $collectionName = $env:COMPUTERNAME + '-' + $tstamp
172 | $collectionsDir = Join-Path $wd 'Collections'
173 | $outputpath = Join-Path $collectionsDir $collectionName
174 | $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
175 | If (Test-Path -Path $wd\Tools) {
176 | }
177 | Else {
178 | Write-Host ' '
179 | Write-Host -For DarkCyan 'Tools directory not present.'
180 | Write-Host ' '
181 | exit 1
182 |
183 | }
184 |
185 | If (Test-Path -Path Collections) {
186 | Write-Host 'Collections directory exists.'
187 | }
188 | Else {
189 | $null = mkdir Collections
190 | If (Test-Path -Path Collections) {
191 | Write-Host 'Collection directory created.'
192 | }
193 | Else {
194 | Write-Host -For DarkCyan 'Error creating directory.'
195 | exit 1
196 | }
197 | }
198 | Set-Location Collections
199 | If (Test-Path -Path $outputpath) {
200 | Write-Host 'Host directory already exists.'
201 | }
202 | Else {
203 | $null = mkdir $outputpath
204 | If (Test-Path -Path $outputpath) {
205 | Write-Host 'Host directory created.'
206 | }
207 | Else {
208 | Write-Host -For DarkCyan 'Error creating directory.'
209 | exit 1
210 | }
211 | }
212 |
213 | # Validate required tools exist
214 | $magnetPath = Join-Path $wd 'Tools\MagnetRESPONSE.exe'
215 | $eddPath = Join-Path $wd 'Tools\EDDv310.exe'
216 | $requiredTools = @(
217 | $magnetPath,
218 | $eddPath
219 | )
220 |
221 | foreach ($tool in $requiredTools) {
222 | if (-not (Test-Path $tool)) {
223 | $toolMsg = 'Required tool not found: {0}' -f $tool
224 | Write-Host -Fore Red $toolMsg
225 | exit 1
226 | }
227 | }
228 |
229 | # Check available disk space on both target drive AND system drive (C:)
230 | $targetDrive = (Get-Item $outputpath).PSDrive
231 | $targetFreeSpaceGB = [math]::Round((Get-PSDrive $targetDrive.Name).Free / 1GB, 2)
232 | $systemFreeSpaceGB = [math]::Round((Get-PSDrive C).Free / 1GB, 2)
233 |
234 | # Get system RAM to estimate space needed (RAM capture needs ~RAM size in temp space)
235 | $totalRAM_GB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
236 |
237 | # Calculate required space based on collection profile
238 | # For RAM collections: need space equal to RAM + 5GB overhead
239 | # For non-RAM collections: 10GB minimum
240 | $targetRequired = 10
241 | $systemRequired = 10
242 |
243 | # Check if this profile includes RAM capture
244 | # Only default, RAMOnly, RAMPage, and RAMSystem actually capture memory
245 | if ($CollectionProfile -eq "" -or $CollectionProfile -match '^RAM') {
246 | # RAM-based or default profile - need more system space
247 | $systemRequired = [math]::Max(($totalRAM_GB + 5), 10)
248 | $targetRequired = [math]::Max(($totalRAM_GB + 10), 15)
249 | }
250 |
251 | $gbUnit = 'GB'
252 | $ramMsg = 'System RAM: {0} {1}' -f $totalRAM_GB, $gbUnit
253 | Write-Host $ramMsg
254 | $targetMsg = 'Target drive {0} free space: {1} {2} (need {3} {4})' -f $targetDrive.Name, $targetFreeSpaceGB, $gbUnit, $targetRequired, $gbUnit
255 | Write-Host $targetMsg
256 | $systemMsg = 'System drive C: free space: {0} {1} (need {2} {3})' -f $systemFreeSpaceGB, $gbUnit, $systemRequired, $gbUnit
257 | Write-Host $systemMsg
258 |
259 | if ($targetFreeSpaceGB -lt $targetRequired) {
260 | $errMsg = 'Insufficient space on target drive. Available: {0} {1} - Required: {2} {3}' -f $targetFreeSpaceGB, $gbUnit, $targetRequired, $gbUnit
261 | Write-Host -Fore Red $errMsg
262 | exit 1
263 | }
264 |
265 | if ($systemFreeSpaceGB -lt $systemRequired) {
266 | $errMsg = 'Insufficient space on system drive (C:). Available: {0} {1} - Required: {2} {3}' -f $systemFreeSpaceGB, $gbUnit, $systemRequired, $gbUnit
267 | Write-Host -Fore Red $errMsg
268 | Write-Host -Fore Red 'MAGNET Response requires approximately RAM-sized space on C: for temporary files during memory capture.'
269 | Write-Host -Fore Yellow 'To proceed anyway, use -CollectionProfile Volatile to skip RAM capture.'
270 | exit 1
271 | }
272 |
273 | ### Pre-Collection Volatile Snapshot (stored in memory for later inclusion in report)
274 | Write-Host -Fore Cyan 'Capturing pre-collection volatile snapshot...'
275 | $collection = $env:COMPUTERNAME + '-' + $tstamp
276 | $preCollectionTime = Get-Date
277 | $snapshotOutput = @()
278 | $snapshotOutput += '=== PRE-COLLECTION VOLATILE SNAPSHOT ==='
279 | $capturedLine = 'Captured: {0}' -f $preCollectionTime.ToString()
280 | $snapshotOutput += $capturedLine
281 | $snapshotOutput += ''
282 |
283 | # System Uptime
284 | $osInfo = Get-CimInstance Win32_OperatingSystem
285 | $uptime = (Get-Date) - $osInfo.LastBootUpTime
286 | $snapshotOutput += '--- SYSTEM UPTIME ---'
287 | $lastBootLine = 'Last Boot: {0}' -f $osInfo.LastBootUpTime
288 | $snapshotOutput += $lastBootLine
289 | $uptimeDetailLine = 'Uptime: {0} days, {1} hours, {2} minutes' -f $uptime.Days, $uptime.Hours, $uptime.Minutes
290 | $snapshotOutput += $uptimeDetailLine
291 | $snapshotOutput += ''
292 |
293 | # Detect Virtual Environment
294 | # Important for analysis: VMs may have memory overcommitment, nested malware, or hypervisor-level threats
295 | # that won't be captured in guest memory dumps. Knowing the environment helps analysts understand
296 | # collection limitations and potential blind spots.
297 | $snapshotOutput += '--- VIRTUALIZATION DETECTION ---'
298 | $computerSystem = Get-CimInstance Win32_ComputerSystem
299 | $modelLine = 'Model: {0}' -f $computerSystem.Model
300 | $snapshotOutput += $modelLine
301 | $mfgLine = 'Manufacturer: {0}' -f $computerSystem.Manufacturer
302 | $snapshotOutput += $mfgLine
303 | if ($computerSystem.Model -match 'Virtual|VMware|VirtualBox|Hyper-V|QEMU|Xen') {
304 | $vmLine = 'Virtual Environment: DETECTED ({0})' -f $computerSystem.Model
305 | $snapshotOutput += $vmLine
306 | $snapshotOutput += 'Note: VM memory dumps may not capture hypervisor-level activity or overcommitted memory'
307 | } else {
308 | $snapshotOutput += 'Virtual Environment: Physical or Unknown'
309 | }
310 | $snapshotOutput += ""
311 |
312 | # Logged-on Users
313 | $snapshotOutput += '--- LOGGED-ON USERS ---'
314 | try {
315 | $loggedOnUsers = Get-CimInstance Win32_LoggedOnUser -ErrorAction Stop |
316 | Select-Object -ExpandProperty Antecedent |
317 | Select-Object -Unique Domain, Name
318 | foreach ($user in $loggedOnUsers) {
319 | $userString = '{0}\{1}' -f $user.Domain, $user.Name
320 | $snapshotOutput += $userString
321 | }
322 | } catch {
323 | $snapshotOutput += 'Unable to enumerate logged-on users'
324 | }
325 | $snapshotOutput += ""
326 |
327 | # Active Network Connections
328 | $snapshotOutput += '--- ACTIVE NETWORK CONNECTIONS ---'
329 | try {
330 | $connections = Get-NetTCPConnection -State Established -ErrorAction Stop |
331 | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, OwningProcess
332 | foreach ($conn in $connections) {
333 | $processName = (Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue).Name
334 | $connLine = '{0}:{1} -> {2}:{3} [{4} PID:{5}]' -f $conn.LocalAddress, $conn.LocalPort, $conn.RemoteAddress, $conn.RemotePort, $processName, $conn.OwningProcess
335 | $snapshotOutput += $connLine
336 | }
337 | } catch {
338 | $snapshotOutput += 'Unable to enumerate network connections'
339 | }
340 | $snapshotOutput += ""
341 |
342 | # Running Processes (top 20 by memory)
343 | $snapshotOutput += '--- TOP PROCESSES (by memory) ---'
344 | try {
345 | $processes = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 20
346 | foreach ($proc in $processes) {
347 | $memMB = [math]::Round($proc.WorkingSet64 / 1MB, 2)
348 | $snapshotOutput += '{0} [PID:{1}] - {2} MB' -f $proc.Name, $proc.Id, $memMB
349 | }
350 | } catch {
351 | $snapshotOutput += 'Unable to enumerate processes'
352 | }
353 |
354 | Write-Host -Fore Cyan 'Pre-collection snapshot captured (will be included in final report)'
355 | Write-Host ''
356 |
357 | ### Collection Profiles
358 | switch ($CollectionProfile) {
359 | 'Volatile' {
360 | $profileName = 'Volatile'
361 | $arguments = '/capturevolatile'
362 | }
363 | 'RAMSystem' {
364 | $profileName = 'RAM and Critical System Files'
365 | $arguments = '/captureram /capturesystemfiles'
366 | }
367 | 'RAMPage' {
368 | $profileName = 'RAM and Pagefile'
369 | $arguments = '/captureram /capturepagefile'
370 | }
371 | 'RAMOnly' {
372 | $profileName = 'RAM Dump'
373 | $arguments = '/captureram'
374 | }
375 | 'QuickTriage' {
376 | $profileName = 'Quick Triage'
377 | $arguments = '/capturevolatile /capturesystemfiles'
378 | }
379 | default {
380 | $profileName = 'MAGNET Triage'
381 | $arguments = '/captureram /capturepagefile /capturevolatile /capturesystemfiles'
382 | }
383 | }
384 |
385 | Write-Host ''
386 | $global:progressPreference = 'silentlyContinue'
387 | Write-Host ''
388 | Write-Host -Fore Cyan 'Running MAGNET Response...'
389 | Write-Host ''
390 | Write-Host ''
391 | Write-Host 'Magnet RESPONSE v1.7'
392 | $magnetCopyright = $copyrightSymbol + '2021-2024 Magnet Forensics Inc'
393 | Write-Host $magnetCopyright
394 | Write-Host ''
395 | $OS = $(((Get-CimInstance Win32_OperatingSystem).Caption).split('|')[0])
396 | $arch = (Get-CimInstance Win32_OperatingSystem).OSArchitecture
397 | $name = (Get-CimInstance Win32_OperatingSystem).CSName
398 | Write-Host ''
399 | Write-Host "Hostname: $name"
400 | Write-Host "Operating System: $OS"
401 | Write-Host "Architecture: $arch"
402 | Write-Host "Selected Profile: $profileName"
403 | Write-Host "Output Directory: $outputpath"
404 | Write-Host ''
405 | Write-Host ''
406 | Write-Host -Fore Cyan 'Collecting Artifacts...'
407 | Write-Host ''
408 |
409 | # Build argument string properly
410 | $outputQuoted = [char]34 + $outputpath + [char]34
411 | $magnetArgs = '/accepteula /unattended /silent /caseref:CyberPipe /output:' + $outputQuoted + ' ' + $arguments
412 | # Use the original working directory we saved
413 | $magnetExePath = Join-Path $wd 'Tools\MagnetRESPONSE.exe'
414 | $magnetProcess = Start-Process -FilePath $magnetExePath -ArgumentList $magnetArgs -PassThru -NoNewWindow
415 |
416 | # Progress indicator while MAGNET Response runs
417 | $elapsed = 0
418 | while (-not $magnetProcess.HasExited) {
419 | Start-Sleep -Seconds 5
420 | $elapsed += 5
421 | $minutes = [math]::Floor($elapsed / 60)
422 | $seconds = $elapsed % 60
423 |
424 | # Show elapsed time and check output folder size (including subfolders)
425 | try {
426 | # Force refresh of directory to avoid cached results
427 | $collectionSize = 0
428 | Get-ChildItem -Path $outputpath -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object {
429 | # Force file info refresh by accessing FileInfo directly
430 | $fileInfo = [System.IO.FileInfo]::new($_.FullName)
431 | $collectionSize += $fileInfo.Length
432 | }
433 |
434 | if ($collectionSize -gt 0) {
435 | # Use MB for collections under 1GB, GB for larger collections
436 | if ($collectionSize -lt 1GB) {
437 | $sizeMB = [math]::Round($collectionSize / 1MB, 1)
438 | $progressMsg = ' [Running: {0} min {1} sec | Collected: {2} MB]' -f $minutes, $seconds, $sizeMB
439 | Write-Host ([char]13 + $progressMsg) -Fore DarkCyan -NoNewline
440 | } else {
441 | $sizeGB = [math]::Round($collectionSize / 1GB, 2)
442 | $progressMsg = ' [Running: {0} min {1} sec | Collected: {2} GB]' -f $minutes, $seconds, $sizeGB
443 | Write-Host ([char]13 + $progressMsg) -Fore DarkCyan -NoNewline
444 | }
445 | } else {
446 | $progressMsg = ' [Running: {0} min {1} sec | Collected: 0.0 MB]' -f $minutes, $seconds
447 | Write-Host ([char]13 + $progressMsg) -Fore DarkCyan -NoNewline
448 | }
449 | }
450 | catch {
451 | $progressMsg = ' [Running: {0} min {1} sec]' -f $minutes, $seconds
452 | Write-Host ([char]13 + $progressMsg) -Fore DarkCyan -NoNewline
453 | }
454 | }
455 | Write-Host '' # New line after progress completes
456 |
457 | $magnetProcess.WaitForExit()
458 |
459 | # PS 5.1 Compatibility: Refresh process and validate exit code
460 | try {
461 | $magnetProcess.Refresh()
462 | } catch {
463 | # Refresh may not be available in all PS versions, ignore error
464 | }
465 |
466 | $magnetExitCode = $magnetProcess.ExitCode
467 |
468 | # Additional validation: Check if files were actually collected (more reliable than exit code alone)
469 | Start-Sleep -Seconds 2
470 | $collectedFiles = Get-ChildItem -Path $outputpath -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
471 | $_.Name -notmatch '(log\.txt|.*temp.*)'
472 | }
473 |
474 | if ($magnetExitCode -ne 0) {
475 | # If exit code is non-zero but files were collected, it may be a PS 5.1 exit code reporting issue
476 | if ($collectedFiles.Count -gt 0) {
477 | $warnMsg = 'MAGNET Response reported exit code {0}, but files were collected successfully. Continuing...' -f $magnetExitCode
478 | Write-Host -Fore Yellow $warnMsg
479 | } else {
480 | # Genuine failure - no files and bad exit code
481 | $errMsg = 'MAGNET Response failed with exit code: {0}' -f $magnetExitCode
482 | Write-Host -Fore Red $errMsg
483 | exit 1
484 | }
485 | }
486 |
487 | $null = $stopwatch.Elapsed
488 | $Minutes = $StopWatch.Elapsed.Minutes
489 | $Seconds = $StopWatch.Elapsed.Seconds
490 | Write-Host ''
491 | $magnetMsg = '** Magnet RESPONSE Completed in {0} minutes and {1} seconds. **' -f $Minutes, $Seconds
492 | Write-Host -Fore Cyan $magnetMsg
493 | Write-Host ''
494 | Write-Host -Fore Cyan 'Running Encrypted Disk Detector (EDD)...'
495 | Write-Host ''
496 | $collection = $env:COMPUTERNAME + '-' + $tstamp
497 | $eddTempFileName = $collection + '-edd-temp.txt'
498 | $eddTempFile = Join-Path $outputpath $eddTempFileName
499 | $eddExePath = Join-Path $wd 'Tools\EDDv310.exe'
500 | $eddProcess = Start-Process -FilePath $eddExePath -ArgumentList '/batch' -RedirectStandardOutput $eddTempFile -PassThru -Wait -NoNewWindow
501 |
502 | if ($eddProcess.ExitCode -ne 0) {
503 | $warnMsg = 'Warning: EDD exited with code {0}' -f $eddProcess.ExitCode
504 | Write-Host -Fore Yellow $warnMsg
505 | }
506 |
507 | Start-Sleep 1
508 | $eddOutput = Get-Content $eddTempFile
509 | $eddOutput | ForEach-Object { Write-Host $_ }
510 | Write-Host ''
511 | Write-Host -Fore Cyan 'Checking for BitLocker Keys...'
512 | Write-Host ''
513 | # Get all BitLocker volumes, not just C:
514 | $bitlockerVolumes = Get-BitLockerVolume | Where-Object { $_.ProtectionStatus -eq 'On' }
515 |
516 | $keyOutput = @()
517 | if ($bitlockerVolumes.Count -eq 0) {
518 | Write-Host -Fore Yellow 'No BitLocker protected volumes found.'
519 | $noBLLine = 'No BitLocker protected volumes found on {0}' -f $env:computername
520 | $keyOutput += $noBLLine
521 | }
522 | else {
523 | foreach ($volume in $bitlockerVolumes) {
524 | $volumeLine = 'Volume: {0}' -f $volume.MountPoint
525 | $keyOutput += $volumeLine
526 | $statusLine = 'Protection Status: {0}' -f $volume.ProtectionStatus
527 | $keyOutput += $statusLine
528 |
529 | if ($volume.KeyProtector.Count -gt 0) {
530 | $keyOutput += 'Key Protectors:'
531 | foreach ($kp in $volume.KeyProtector) {
532 | $typeLine = ' Type: {0}' -f $kp.KeyProtectorType
533 | $keyOutput += $typeLine
534 | if ($kp.RecoveryPassword) {
535 | $pwdLine = ' Recovery Password: {0}' -f $kp.RecoveryPassword
536 | $keyOutput += $pwdLine
537 | }
538 | if ($kp.KeyProtectorId) {
539 | $idLine = ' Key ID: {0}' -f $kp.KeyProtectorId
540 | $keyOutput += $idLine
541 | }
542 | }
543 | $blMsg = 'BitLocker key(s) recovered for volume {0}' -f $volume.MountPoint
544 | Write-Host -Fore Cyan $blMsg
545 | }
546 | else {
547 | $keyOutput += 'No key protectors found for this volume.'
548 | $noKeyMsg = 'No key protectors for volume {0}' -f $volume.MountPoint
549 | Write-Host -Fore Yellow $noKeyMsg
550 | }
551 | $keyOutput += ""
552 | }
553 | }
554 | Set-Location ~
555 | $StopWatch.Stop()
556 | $null = $stopwatch.Elapsed
557 | $Minutes = $StopWatch.Elapsed.Minutes
558 | $Seconds = $StopWatch.Elapsed.Seconds
559 | Write-Host ''
560 | $completionMsg = '*** Collection Completed in {0} minutes and {1} seconds. ***' -f $Minutes, $Seconds
561 | Write-Host -Fore Cyan $completionMsg
562 | Write-Host ''
563 |
564 | # Generate Comprehensive CyberPipe Report
565 | Write-Host -Fore Cyan 'Generating CyberPipe collection report...'
566 | $reportFile = Join-Path $outputpath 'CyberPipe-Report.txt'
567 | $reportOutput = @()
568 |
569 | # Header
570 | $separator = '=' * 80
571 | $reportOutput += $separator
572 | $reportOutput += 'CYBERPIPE INCIDENT RESPONSE COLLECTION REPORT'
573 | $reportOutput += $separator
574 | $reportOutput += ''
575 | $hostLine = 'Host: {0}' -f $env:COMPUTERNAME
576 | $reportOutput += $hostLine
577 | $profileLine = 'Collection Profile: {0}' -f $profileName
578 | $reportOutput += $profileLine
579 | $collectionStarted = 'Collection Started: {0}' -f $preCollectionTime.ToString()
580 | $reportOutput += $collectionStarted
581 | $collectionCompleted = 'Collection Completed: {0}' -f (Get-Date).ToString()
582 | $reportOutput += $collectionCompleted
583 | $durationLine = 'Duration: {0} minutes, {1} seconds' -f $Minutes, $Seconds
584 | $reportOutput += $durationLine
585 | $reportOutput += 'Generated by: CyberPipe v5.3'
586 | $reportOutput += 'https://github.com/dwmetz/CyberPipe'
587 | $reportOutput += ''
588 | $reportOutput += $separator
589 |
590 | # Pre-Collection Volatile Snapshot
591 | $reportOutput += ''
592 | $reportOutput += $snapshotOutput
593 | $reportOutput += ''
594 | $reportOutput += $separator
595 |
596 | # Collection Summary
597 | $reportOutput += ''
598 | $reportOutput += '=== COLLECTION SUMMARY ==='
599 | $reportOutput += ''
600 | $allFiles = Get-ChildItem -Path $outputpath -Recurse -File
601 | $totalSize = ($allFiles | Measure-Object -Property Length -Sum).Sum
602 | $totalSizeGB = [math]::Round($totalSize / 1GB, 2)
603 | $fileCount = $allFiles.Count
604 |
605 | $filesLine = 'Total Files Collected: {0}' -f $fileCount
606 | $reportOutput += $filesLine
607 | $reportOutput += 'Total Collection Size: {0} GB' -f $totalSizeGB
608 | $reportOutput += 'System RAM: {0} GB' -f $totalRAM_GB
609 | $uptimeLine = 'Uptime at Collection: {0} days, {1} hours' -f $uptime.Days, $uptime.Hours
610 | $reportOutput += $uptimeLine
611 | $virtualEnvLine = 'Virtual Environment: {0}' -f $virtualEnv
612 | $reportOutput += $virtualEnvLine
613 | $reportOutput += ""
614 |
615 | # Files by Type
616 | $reportOutput += '--- FILES BY TYPE ---'
617 | $filesByType = $allFiles | Group-Object Extension | Sort-Object Count -Descending
618 | foreach ($group in $filesByType) {
619 | $ext = if ($group.Name) { $group.Name } else { '(no extension)' }
620 | $groupSize = ($group.Group | Measure-Object -Property Length -Sum).Sum
621 | $groupSizeGB = [math]::Round($groupSize / 1GB, 3)
622 | $reportOutput += '{0} : {1} files {2} GB' -f $ext, $group.Count, $groupSizeGB
623 | }
624 | $reportOutput += ""
625 |
626 | # Key Artifacts
627 | $reportOutput += '--- KEY ARTIFACTS ---'
628 | $keyArtifacts = $allFiles | Where-Object { $_.Name -match '(\.raw|\.mem|\.dmp|\.txt|\.json|\.csv)' }
629 | foreach ($artifact in $keyArtifacts) {
630 | if ($artifact.Length -lt 1MB) {
631 | $sizeKB = [math]::Round($artifact.Length / 1KB, 2)
632 | $reportOutput += '{0} {1} KB' -f $artifact.Name, $sizeKB
633 | } else {
634 | $sizeMB = [math]::Round($artifact.Length / 1MB, 2)
635 | $reportOutput += '{0} {1} MB' -f $artifact.Name, $sizeMB
636 | }
637 | }
638 | $reportOutput += ""
639 | $reportOutput += $separator
640 |
641 | # Encrypted Disk Detection
642 | $reportOutput += ""
643 | $reportOutput += '=== ENCRYPTED DISK DETECTION ==='
644 | $reportOutput += ""
645 | $reportOutput += $eddOutput
646 | $reportOutput += ""
647 | $reportOutput += $separator
648 |
649 | # BitLocker Recovery Keys
650 | $reportOutput += ""
651 | $reportOutput += '=== BITLOCKER RECOVERY KEYS ==='
652 | $reportOutput += ""
653 | $reportOutput += $keyOutput
654 | $reportOutput += ""
655 | $reportOutput += $separator
656 |
657 | # SHA256 Hashes
658 | $reportOutput += ""
659 | $reportOutput += '=== SHA256 INTEGRITY HASHES ==='
660 | $reportOutput += ""
661 |
662 | Get-ChildItem -Path $outputpath -Recurse -File | ForEach-Object {
663 | try {
664 | $hash = Get-FileHash -Path $_.FullName -Algorithm SHA256 -ErrorAction Stop
665 | $pathPrefix = $outputpath + '\'
666 | $relativePath = $_.FullName.Replace($pathPrefix, '')
667 | $hashLine = '{0} {1}' -f $hash.Hash, $relativePath
668 | $reportOutput += $hashLine
669 | }
670 | catch {
671 | $warnMsg = 'Warning: Could not hash file {0}' -f $_.Name
672 | Write-Host -Fore Yellow $warnMsg
673 | $errLine = 'ERROR: Could not hash {0}' -f $_.Name
674 | $reportOutput += $errLine
675 | }
676 | }
677 |
678 | $reportOutput += ""
679 | $reportOutput += $separator
680 | $reportOutput += 'END OF REPORT'
681 | $reportOutput += $separator
682 |
683 | $reportOutput | Out-File -FilePath $reportFile -Encoding UTF8
684 | Write-Host -Fore Cyan 'Comprehensive report created: CyberPipe-Report.txt'
685 |
686 | # Clean up temporary EDD file
687 | if (Test-Path $eddTempFile) {
688 | Remove-Item $eddTempFile -Force -ErrorAction SilentlyContinue
689 | }
690 |
691 | $durationString = '{0} min {1} sec' -f $Minutes, $Seconds
692 | $uptimeString = '{0} days, {1} hours' -f $uptime.Days, $uptime.Hours
693 | $virtualEnv = if ($computerSystem.Model -match 'Virtual|VMware|VirtualBox|Hyper-V|QEMU|Xen') { $computerSystem.Model } else { 'Physical/Unknown' }
694 |
695 | $summary = @{
696 | Hostname = $name
697 | OS = $OS
698 | Architecture = $arch
699 | Profile = $profileName
700 | CollectionStarted = $preCollectionTime.ToString()
701 | CollectionCompleted = (Get-Date).ToString()
702 | Duration = $durationString
703 | TotalFiles = $fileCount
704 | TotalSizeGB = $totalSizeGB
705 | Uptime = $uptimeString
706 | VirtualEnvironment = $virtualEnv
707 | ReportFile = 'CyberPipe-Report.txt'
708 | Status = 'Completed'
709 | }
710 | $summaryFile = Join-Path $outputpath 'collection-summary.json'
711 | $summary | ConvertTo-Json | Set-Content $summaryFile
712 |
713 | # Add CSV header if file does not exist
714 | $csvPath = Join-Path $wd 'CyberPipe-runs.csv'
715 | if (-not (Test-Path $csvPath)) {
716 | $csvHeader = 'Timestamp,Hostname,Profile,Duration'
717 | Set-Content -Path $csvPath -Value $csvHeader
718 | }
719 |
720 | $comma = [char]44
721 | $colon = [char]58
722 | $csvLine = (Get-Date).ToString() + $comma + $env:COMPUTERNAME + $comma + $profileName + $comma + $Minutes + $colon + $Seconds
723 | $csvPath = Join-Path $wd 'CyberPipe-runs.csv'
724 | Add-Content -Path $csvPath -Value $csvLine
725 |
726 | # Optional Compression
727 | if ($Compress) {
728 | Write-Host -Fore Cyan 'Compressing collection...'
729 | $collectionsDir = Join-Path $wd 'Collections'
730 | $zipFileName = $collection + '.zip'
731 | $zipPath = Join-Path $collectionsDir $zipFileName
732 |
733 |
734 | # Check collection size first to determine compression strategy
735 | $collectionSize = (Get-ChildItem -Path $outputpath -Recurse -File | Measure-Object -Property Length -Sum).Sum
736 | $collectionSizeGB = [math]::Round($collectionSize / 1GB, 2)
737 |
738 | $sizeMsg = ' Collection size: {0} {1}' -f $collectionSizeGB, $gbUnit
739 | Write-Host $sizeMsg
740 |
741 | # Try 7-Zip first if available (handles large files, better compression)
742 | $localToolPath = Join-Path $wd 'Tools\7z.exe'
743 | $sevenZipPaths = @(
744 | $localToolPath,
745 | 'C:\Program Files\7-Zip\7z.exe',
746 | 'C:\Program Files (x86)\7-Zip\7z.exe'
747 | )
748 |
749 | $sevenZipExe = $sevenZipPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
750 |
751 | if ($sevenZipExe) {
752 | Write-Host -Fore Cyan ' Using 7-Zip for compression (supports large files)...'
753 | try {
754 | # Use 7-Zip with ZIP64 format (no size limit)
755 | # -tzip = ZIP format, -mx5 = medium compression (balance speed/ratio)
756 | $sourcePattern = $outputpath + '\*'
757 | $zipPathQuoted = [char]34 + $zipPath + [char]34
758 | $sourceQuoted = [char]34 + $sourcePattern + [char]34
759 | $7zArgs = 'a -tzip -mx5 ' + $zipPathQuoted + ' ' + $sourceQuoted
760 | $7zProcess = Start-Process -FilePath $sevenZipExe -ArgumentList $7zArgs -Wait -PassThru -NoNewWindow
761 |
762 | if ($7zProcess.ExitCode -eq 0 -and (Test-Path $zipPath)) {
763 | $zipSize = [math]::Round((Get-Item $zipPath).Length / 1GB, 2)
764 | $successMsg = 'Collection compressed: {0}.zip - {1} {2}' -f $collection, $zipSize, $gbUnit
765 | Write-Host -Fore Green $successMsg
766 |
767 | # Optionally remove uncompressed folder
768 | # Uncomment the following lines to auto-delete after compression:
769 | # Remove-Item -Path $outputpath -Recurse -Force
770 | # Write-Host -Fore Cyan 'Uncompressed collection removed.'
771 | } else {
772 | $errMsg = '7-Zip exited with code {0}' -f $7zProcess.ExitCode
773 | throw $errMsg
774 | }
775 | }
776 | catch {
777 | $errMsg = '7-Zip compression failed: {0}' -f $_.Exception.Message
778 | Write-Host -Fore Red $errMsg
779 | $remainsMsg = 'Uncompressed collection remains at: {0}' -f $outputpath
780 | Write-Host -Fore Yellow $remainsMsg
781 | }
782 | }
783 | # Fall back to Compress-Archive only for small collections (< 1.5GB)
784 | elseif ($collectionSizeGB -lt 1.5) {
785 | Write-Host -Fore Cyan ' Using built-in compression (small collection)...'
786 | try {
787 | Compress-Archive -Path $outputpath -DestinationPath $zipPath -CompressionLevel Optimal -Force
788 | $zipSize = [math]::Round((Get-Item $zipPath).Length / 1GB, 2)
789 | $successMsg = 'Collection compressed: {0}.zip - {1} {2}' -f $collection, $zipSize, $gbUnit
790 | Write-Host -Fore Green $successMsg
791 |
792 | # Optionally remove uncompressed folder
793 | # Uncomment the following lines to auto-delete after compression:
794 | # Remove-Item -Path $outputpath -Recurse -Force
795 | # Write-Host -Fore Cyan 'Uncompressed collection removed.'
796 | }
797 | catch {
798 | $errMsg = 'Compression failed: {0}' -f $_.Exception.Message
799 | Write-Host -Fore Red $errMsg
800 | $remainsMsg = 'Uncompressed collection remains at: {0}' -f $outputpath
801 | Write-Host -Fore Yellow $remainsMsg
802 | }
803 | }
804 | else {
805 | $tooLargeMsg = 'Collection is too large - {0} GB - for built-in compression.' -f $collectionSizeGB
806 | Write-Host -Fore Yellow $tooLargeMsg
807 | Write-Host -Fore Yellow 'PowerShell Compress-Archive cannot create archives larger than 2GB.'
808 | Write-Host -Fore Yellow ""
809 | Write-Host -Fore Yellow 'To compress large collections, install 7-Zip:'
810 | Write-Host -Fore Yellow ' 1. Download from https://www.7-zip.org/'
811 | Write-Host -Fore Yellow ' 2. Install to default location, OR'
812 | $toolsMsg = ' 3. Copy 7z.exe to {0}\Tools\' -f $wd
813 | Write-Host -Fore Yellow $toolsMsg
814 | Write-Host -Fore Yellow ""
815 | $remainsMsg = 'Uncompressed collection remains at: {0}' -f $outputpath
816 | Write-Host -Fore Yellow $remainsMsg
817 | }
818 | }
819 |
820 | # Validate collection actually succeeded by checking for artifacts
821 | if (-not (Test-Path $outputpath)) {
822 | Write-Host -Fore Red 'Collection failed: Output directory not found.'
823 | exit 1
824 | }
825 |
826 | # Check that we have more than just our own generated files (report, summary, log)
827 | $collectedFiles = Get-ChildItem -Path $outputpath -Recurse -File | Where-Object {
828 | $_.Name -notmatch '(CyberPipe-Report|summary\.json|log\.txt)'
829 | }
830 |
831 | if ($collectedFiles.Count -eq 0) {
832 | Write-Host -Fore Red 'Collection failed: No artifacts collected by MAGNET Response.'
833 | $logFile = Join-Path $outputpath 'log.txt'
834 | $checkMsg = 'Check {0} for details.' -f $logFile
835 | Write-Host -Fore Red $checkMsg
836 | exit 1
837 | }
838 |
839 | # Check if this was a RAM collection and verify memory dump exists
840 | if ($CollectionProfile -match 'RAM|^$') {
841 | $memoryDump = Get-ChildItem -Path $outputpath -Recurse -File | Where-Object {
842 | $_.Extension -match '\.(raw|mem|dmp|bin)' -and $_.Length -gt 100MB
843 | }
844 |
845 | if (-not $memoryDump) {
846 | Write-Host -Fore Yellow 'Warning: RAM collection selected but no memory dump found (or dump under 100MB).'
847 | $logPath = Join-Path $outputpath 'log.txt'
848 | $logMsg = 'Collection may have failed. Check {0} for details.' -f $logPath
849 | Write-Host -Fore Yellow $logMsg
850 | }
851 | }
852 |
853 | # Calculate total collection size for better reporting
854 | $collectionSizeGB = [math]::Round(($collectedFiles | Measure-Object -Property Length -Sum).Sum / 1GB, 2)
855 |
856 | Write-Host -Fore Green 'Collection succeeded.'
857 | $profileMsg = ' Profile: {0}' -f $profileName
858 | Write-Host -Fore Green $profileMsg
859 | $successMsg = ' Files: {0} - Size: {1} {2}' -f $collectedFiles.Count, $collectionSizeGB, $gbUnit
860 | Write-Host -Fore Green $successMsg
861 | Write-Host ''
862 | exit 0
863 |
--------------------------------------------------------------------------------