├── .gitignore ├── images ├── BSF.png ├── CyberPipe.png ├── Screenshot_v4.01.png └── CyberPipe_screenshot.png ├── LICENSE ├── README_v4.01.md ├── README.md ├── CyberPipe.v4.01.ps1 └── CyberPipe.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /images/BSF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmetz/CyberPipe/HEAD/images/BSF.png -------------------------------------------------------------------------------- /images/CyberPipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmetz/CyberPipe/HEAD/images/CyberPipe.png -------------------------------------------------------------------------------- /images/Screenshot_v4.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmetz/CyberPipe/HEAD/images/Screenshot_v4.01.png -------------------------------------------------------------------------------- /images/CyberPipe_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwmetz/CyberPipe/HEAD/images/CyberPipe_screenshot.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Doug Metz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README_v4.01.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 |

5 | CyberPipe 6 |

7 |
8 | An easy to use PowerShell script to collect memory and disk forensics for DFIR investigations. 9 |
10 |

11 |

12 |

13 |
14 | 15 |
16 |
17 | Functions: 18 |
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 |
26 | Prerequisites: 27 |
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 |

37 | v4.0 Features: “One Script to Rule them All” 38 |

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 |
53 | Network Collections: 54 |
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 |
2 | 3 | 4 |

5 | CyberPipe v5.3 6 |

7 |
8 | (formerly CSIRT-Collect) 9 |
10 | 11 |
12 | An easy to use PowerShell script to collect memory and disk forensics for DFIR investigations. 13 |
14 |

15 |

16 |

17 | 18 |
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 | --------------------------------------------------------------------------------