├── README.md ├── Invoke-BitsDetection.ps1 ├── Invoke-BitsTransfer.ps1 └── Invoke-BitsParser.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # Invoke-BitsParser 2 | A few scripts I have written to parse various Windows Background Intelligent Transfer Service (BITS) artefacts. 3 | 4 | ### Invoke-BitsParser.ps1 5 | * Parses BITS jobs from QMGR files. 6 | * QMGR files are named qmgr0.dat or qmgr1.dat and located in the folder %ALLUSERSPROFILE%\Microsoft\Network\Downloader. 7 | * Capable to run in live mode or against precollected files. 8 | * Initial Python parser used as inspiration by ANSSI here - https://github.com/ANSSI-FR/bits_parser 9 | * Invoke-BitsParser currently does not carve non complete jobs. 10 | * Currently not supported on Windows 10+ 11 | 12 | ### Invoke-BitsTransfer.ps1 13 | * carves BITS file transfer details from QMGR files. 14 | * QMGR files are named qmgr0.dat or qmgr1.dat and located in the folder %ALLUSERSPROFILE%\Microsoft\Network\Downloader. 15 | * Finding XferHeaders, Invoke-BitsTransfer then searches for valid paths. 16 | * Run to carve all File transfer information - Source and Destination. Then look for unusual entries. 17 | * Currently not supported on Windows 10+ 18 | 19 | ### Invoke-BitsDetection.ps1 20 | * Extracts all BITS transfer URLs from Windows BITS Event log. 21 | * Microsoft-Windows-Bits-Client/Operational Event ID 59. 22 | * Whitelist of commonly used domains, please add new domains. 23 | * The goal of this script is to detect anomolous sources for BITs transfers that can lead to additional investigation. 24 | * Use -All switch to list all URL sources and do not run Whitleist check. 25 | * Supported accross all Windows versions 26 | 27 | -------------------------------------------------------------------------------- /Invoke-BitsDetection.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Invoke-BitsDetection.ps1 detects on anomalous BITS transfer URLs from Windows BITS Event log. 4 | 5 | Name: Invoke-BitsDetection.ps1 6 | Version: 0.1 7 | Author: Matt Green (@mgreen27) 8 | 9 | .DESCRIPTION 10 | Invoke-BitsDetection.ps1 extracts all BITS transfer URLs from Windows Event log. 11 | Microsoft-Windows-Bits-Client/Operational Event ID 59. 12 | 13 | Utilising a WHitelist of expected BITs domains, the goal of this script is to detect anomolous sources for BITs transfers that can lead to additional investigation. 14 | Use -All switch to list all URL sources and do not run Whitleist check. 15 | 16 | Please add new domains to whitelist. 17 | 18 | .PARAMETER BackDays 19 | Optional parameter for specifying number of days to search back in Microsoft-Windows-Bits-Client/Operational Eventlog. Default is 14. 20 | 21 | .PARAMETER All 22 | List all URLs and bypass whitelist comparison. 23 | 24 | .EXAMPLE 25 | Invoke-BitsDetection.ps1 26 | 27 | Run Invoke-BitsDetection and alert on anomolous or new source urls. 28 | 29 | .EXAMPLE 30 | Invoke-BitsDetection.ps1 -Backdays 7 31 | 32 | Run Invoke-BitsDetection back 7 days of event logs from current date. 33 | 34 | .EXAMPLE 35 | Invoke-BitsDetection.ps1 -All 36 | 37 | Run Invoke-BitsDetection and show all URLs unfiltered. 38 | .NOTES 39 | 40 | #> 41 | [CmdletBinding()] 42 | Param ( 43 | [Parameter(Mandatory=$False)][int32]$BackDays=14, 44 | [Parameter(Mandatory = $False)][Switch]$All = $False 45 | ) 46 | 47 | # Set whitelist 48 | $Whitelist = @( 49 | "http*://aka.ms/*",# Microsoft site 50 | "http*://img-prod-cms-rt-microsoft-com.akamaized.net/*",# Microsoft on Akamai 51 | "http*://img-s-msn-com.akamaized.net/*",# MSN on Akamai 52 |   "http*://*.adobe.com/*",# adobe 53 | "http*://*.adobe.com/*",# adobe 54 | "http*://*.amazon.com/*",# Amazon corporate 55 | "http*://*.apache.org/*",# Apache 56 | "http*://*.avast.com/*",# Avast 57 | "http*://*.avcdn.net/*",# Avast cdn 58 | "http*://*.bing.com/*",# Microsoft Bing 59 | "http*://*.core.windows.net/*",# Microsoft site 60 | "http*://*.fbcdn.net/*",# Facebook cdn 61 | "http*://*.google.com/*",# Google 62 | "http*://*.googleapis.com/*",# Google api domain 63 | "http*://*.googleusercontent.com/*",# GoogleUsercontent 64 | "http*://*.gvt1.com/*"# Google chrome 65 | "http*://*.hp.com/*",# HP domain 66 | "http*://*.live.com/*",# Microsoft Live 67 | "http*://*.microsoft.com/*",# Microsoft site 68 | "http*://*.msn.com/*",# MSN 69 | "http*://*.nero.com/*",# Nero software 70 | "http*://*.office365.com/*",# Microsoft office 365 71 | "http*://*.onenote.net/*",# OneNote cdn 72 | "http*://*.oracle.com/*",# Oracle domain 73 | "http*://*.s-msn.com/*",# MSN 74 | "http*://*.symantec.com/*",# Symantec 75 | "http*://*.thomsonreuters.com/*",# News site 76 | "http*://*.visualstudio.com/*",# Microsoft VisualStudio 77 | "http*://*.windowsupdate.com/*",# Windows update 78 | "http*://*.xboxlive.com/*"# Microsoft site 79 | ) 80 | 81 | $All = $PSBoundParameters.ContainsKey("All") 82 | 83 | $Results = $False 84 | $BackTime=(Get-Date) - (New-TimeSpan -Days $BackDays) 85 | 86 | $RawEvents = Get-WinEvent -LogName "Microsoft-Windows-Bits-Client/Operational" | Where-Object {$_.TimeCreated -ge $BackTime} | Where-Object { $_.Id -eq 59} 87 | 88 | 89 | $RawEvents | ForEach-Object { 90 | 91 | If(!$All){ 92 | Foreach ($Url in $Whitelist){ 93 | If ($_.Properties[3].Value -like $Url ){return} 94 | } 95 | } 96 | 97 | $Results = $True 98 | 99 | $PropertyBag = [ordered] @{ 100 | TimeUTC = Get-Date (($_.TimeCreated).ToUniversalTime()) -Format s 101 | TransferId = $_.Properties[0].Value 102 | Name = $_.Properties[1].Value 103 | Id = $_.Properties[2].Value 104 | URL = $_.Properties[3].Value 105 | FileTime = $_.Properties[5].Value 106 | FileLength = $_.Properties[6].Value 107 | BytesTotal = $_.Properties[7].Value 108 | BytesTransferred = $_.Properties[8].Value 109 | } 110 | 111 | $Output = New-Object -TypeName PSCustomObject -Property $PropertyBag 112 | 113 | # When modifying PropertyBag remember to change Seldect-Object for ordering below 114 | $Output | Select-Object TimeUTC,Name,URL 115 | [gc]::Collect() 116 | } 117 | 118 | If(!$Results){"Invoke-BitsDetecton: No anomolous BITS activity deteted."} -------------------------------------------------------------------------------- /Invoke-BitsTransfer.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Invoke-BitsTransfer.ps1 carves BITS file transfers from QMGR queue and alerts on suspicious entries. 4 | 5 | Name: Invoke-BitsTransfer.ps1 6 | Version: 0.1 (POC) 7 | Author: Matt Green (@mgreen27) 8 | 9 | .DESCRIPTION 10 | Invoke-BitsTransfer.ps1 carves BITS file transfer details from QMGR files. 11 | Finding XferHeaders, Invoke-BitsTransfer then searches for valid paths and outputs to stdout. 12 | 13 | QMGR files are named qmgr0.dat or qmgr1.dat and located in the folder %ALLUSERSPROFILE%\Microsoft\Network\Downloader. 14 | Run to carve all File transfer information - Source and Destination. Then look for unusual entries. 15 | Currently not supported on Windows 10 with different formatting. 16 | 17 | .PARAMETER Path 18 | Use this parameter to run against a previously collected QMgr file. 19 | 20 | .EXAMPLE 21 | Invoke-BitsTransfer.ps1 22 | 23 | Run Invoke-BitsTransfer in report all mode. Will report all Files found after carving xferheaders 24 | 25 | .EXAMPLE 26 | Invoke-BitsTransfer.ps1 -Path c:\cases\bits\qmgr0.dat 27 | 28 | Run Invoke-BitsTransfer in offline mode 29 | 30 | .NOTES 31 | Initial Python parser used as inspiration by ANSSI here - https://github.com/ANSSI-FR/bits_parser 32 | Invoke-BitsTransfer currently only carves XferHeaders. 33 | 34 | XferHeader (32): 36-DA-56-77-6F-51-5A-43-AC-AC-44-A2-48-FF-F3-4D 35 | #> 36 | 37 | [CmdletBinding()] 38 | Param( 39 | [Parameter(Mandatory = $False)][String]$Path="$env:ALLUSERSPROFILE\Microsoft\Network\Downloader", 40 | [Parameter(Mandatory = $False)][Switch]$LiveMode = $True, 41 | [Parameter(Mandatory = $False)][Switch]$All 42 | 43 | ) 44 | 45 | # Set switches 46 | $Verbose = $PSBoundParameters.ContainsKey("Verbose") 47 | If ($PSBoundParameters.ContainsKey("Path")){$LiveMode = $False} 48 | 49 | # Test for Elevated privilege if required 50 | If ($Path -eq "$env:ALLUSERSPROFILE\Microsoft\Network\Downloader"){ 51 | If (!(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))){ 52 | Write-Host -ForegroundColor Red "Exiting Invoke-BitsDetect: Elevated privilege required for LiveResponse Mode" 53 | exit 54 | } 55 | } 56 | 57 | # Determine files in scope 58 | if(Test-Path $Path -pathType container){ 59 | $QmgrFiles = (Get-ChildItem $Path -Filter qmgr*.dat).FullName 60 | } 61 | ElseIf(Test-Path $Path -pathType Leaf){ 62 | $QmgrFiles = $Path 63 | } 64 | 65 | 66 | ## Regex setup 67 | # Chunk value for Memory optimised regex increment. Keep >= 2500 68 | $Chunk = 25000 69 | 70 | If ($Chunk -lt 25000){ 71 | Write-Host -ForegroundColor Red "Exiting Invoke-BitsDetectr: Regex `$Chunk of $Chunk is too low!" 72 | exit 73 | } 74 | 75 | $XferHeader = "36DA56776F515A43ACAC44A248FFF34D" 76 | 77 | ## Main 78 | ForEach($Path in $QmgrFiles){ 79 | 80 | # Resetting Hex stream, variables and results for each QMgrFile 81 | $Hex = $null 82 | $Position = $null 83 | $Output = @{} 84 | 85 | If ($Verbose){Write-Host -ForegroundColor Cyan "Parsing $Path"} 86 | 87 | # Adding QmanagerFile to a Hex Array 88 | $FileStream = New-Object System.IO.FileStream -ArgumentList ($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) 89 | $BinaryReader = New-Object System.IO.BinaryReader $FileStream 90 | $Hex = [System.BitConverter]::ToString($BinaryReader.ReadBytes($FileStream.Length)) -replace "-","" 91 | 92 | # Dispose FileStreams 93 | $FileStream.Dispose() 94 | $BinaryReader.Dispose() 95 | [gc]::Collect() 96 | 97 | 98 | 99 | # Using XferHeader to find interesting BITS activity 100 | $XferHeaderHits = $Null 101 | $a = 0 102 | $b = $Chunk 103 | 104 | While ($a -lt $Hex.Length){ 105 | [gc]::Collect() 106 | $XferHeaderHits = $XferHeaderHits + [regex]::Matches(($Hex[$a..$b] -join ""),$XferHeader).Index 107 | 108 | # Adding chunk minus xferheader less 1 109 | $a = $a + $Chunk -31 110 | $b = $b + $Chunk -31 111 | } 112 | [gc]::Collect() 113 | 114 | 115 | # If NoXFerHeaderHits we are done 116 | $XferHeaderHits = $XferHeaderHits | Where-Object {$_} 117 | If(!$XferHeaderHits){break} 118 | 119 | 120 | Foreach($Hit in $XferHeaderHits){ 121 | $Position = $Null 122 | 123 | # Needing to account for Regex starting from $Position previously 124 | $Position = $Hit + 32 125 | 126 | # FilesCount in 1 byte (with Endian black magic) 127 | $FilesCount = $null 128 | $FilesCount = $Hex[$Position..($Position + 7)] -join "" 129 | $FilesCount = $FilesCount[6..7] + $FilesCount[4..5] + $FilesCount[2..3] + $FilesCount[0..1] 130 | 131 | Try{$FilesCount = [convert]::toint16($FilesCount -join "",16)} 132 | Catch{$FileCount = $null} 133 | 134 | $Position = $Position + 8 135 | 136 | 137 | # Some Bits versions will have Jobs with no filecount and requre carving. Jumping for now. 138 | If($FilesCount.GetType().fullname -eq "System.Int16" -and $FilesCount -gt 0){ 139 | 140 | # DestLength in 2 bytes (With Endian black magic) 141 | $DestLength = $Null 142 | $DestLength = $Hex[$Position..($Position + 7)] -join "" 143 | $DestLength = $DestLength[6..7] + $DestLength[4..5] + $DestLength[2..3] + $DestLength[0..1] 144 | 145 | Try{$DestLength = [convert]::toint16($DestLength -join "",16)} 146 | Catch{$DestLength = $Null} 147 | 148 | $Position = $Position + 8 149 | 150 | 151 | # DestPath - Parsing each character of DestPath from 1 bytes x $DestPathLength characters 152 | $DestPath = $Null 153 | 154 | For($Count = 0;$Count -lt ($DestLength -1);$Count++){ 155 | 156 | Try{$DestPath = $DestPath + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 157 | Catch{Continue} 158 | 159 | $Position = $Position + 4 160 | } 161 | 162 | $Position = $Position + 4 163 | 164 | 165 | # SourceLength in 2 bytes (With Endian black magic) 166 | $SourceLength = $Null 167 | $SourceLength = $Hex[$Position..($Position + 7)] -join "" 168 | $SourceLength = $SourceLength[6..7] + $SourceLength[4..5] + $SourceLength[2..3] + $SourceLength[0..1] 169 | 170 | Try{$SourceLength = [convert]::toint16($SourceLength -join "",16)} 171 | Catch{$SourceLength = $Null} 172 | 173 | 174 | $Position = $Position + 8 175 | 176 | 177 | # SourcePath - Parsing each character of SourcePath from 1 bytes x SourceLength characters 178 | $SourcePath = $Null 179 | 180 | For($Count = 0;$Count -lt ($SourceLength -1);$Count++){ 181 | 182 | Try{$SourcePath = $SourcePath + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 183 | Catch{Continue} 184 | 185 | $Position = $Position + 4 186 | } 187 | 188 | $Position = $Position + 4 189 | 190 | 191 | # Setting Sourcepath and Destpath to null if space characters 192 | If($SourcePath -ne $Null){ 193 | If($SourcePath.GetType().FullName -eq "System.Char"){$SourcePath = $Null} 194 | Else{ 195 | If($SourcePath.replace(" ","") -eq $Null ){$SourcePath = $null} 196 | } 197 | } 198 | 199 | If($DestPath -ne $Null){ 200 | If($DestPath.GetType().FullName -eq "System.Char"){$DestPath = $Null} 201 | Else{ 202 | If($DestPath.replace(" ","") -eq $Null ){$DestPath = $null} 203 | } 204 | } 205 | 206 | 207 | # IF source null or source length less than 10 (indicates miss parse) then continue to next item 208 | If($SourcePath -eq $Null -Or $SourcePath.length -lt 10){continue} 209 | 210 | 211 | # Display Results 212 | $Output = [ordered] @{ 213 | QmgrFile = $Path 214 | Source = $SourcePath 215 | Destination = $DestPath 216 | } 217 | 218 | $Output | Format-Table -AutoSize -Wrap 219 | 220 | [gc]::Collect() 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /Invoke-BitsParser.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Invoke-BitsParser.ps1 parses BITS jobs from QMGR queue. 4 | 5 | Name: Invoke-BitsParser.ps1 6 | Version: 0.3 7 | Author: Matt Green (@mgreen27) 8 | 9 | .DESCRIPTION 10 | Invoke-BitsParser.ps1 parses BITS jobs from QMGR files. 11 | QMGR files are named qmgr0.dat or qmgr1.dat and located in the folder %ALLUSERSPROFILE%\Microsoft\Network\Downloader. 12 | Capable to run in live mode or against precollected files. 13 | 14 | .PARAMETER Path 15 | Use this parameter to run against a previously collected QMgr file. 16 | 17 | .EXAMPLE 18 | Invoke-BitsParser.ps1 19 | 20 | Run Invoke-BitsParser in live mode. 21 | 22 | .EXAMPLE 23 | Invoke-BitsParser.ps1 -Path c:\cases\bits\qmgr0.dat 24 | 25 | Run Invoke-BitsParser in offline mode 26 | 27 | .EXAMPLE 28 | Invoke-BitsParser.ps1 -verbose 29 | 30 | Run Invoke-BitsParser in verbose mode. 31 | 32 | .NOTES 33 | Initial Python parser used as inspiration by ANSSI here - https://github.com/ANSSI-FR/bits_parser 34 | Invoke-BitsParser currently does not carve incomplete Jobs. 35 | 36 | FileHeader (32): 13-F7-2B-C8-40-99-12-4A-9F-1A-3A-AE-BD-89-4E-EA 37 | QueueHeader (32): 47-44-5F-00-A9-BD-BA-44-98-51-C4-7B-B6-C0-7A-CE 38 | Header (64): 13-F7-2B-C8-40-99-12-4A-9F-1A-3A-AE-BD-89-4E-EA-47-44-5F-00-A9-BD-BA-44-98-51-C4-7B-B6-C0-7A-CE 39 | XferHeader (32): 36-DA-56-77-6F-51-5A-43-AC-AC-44-A2-48-FF-F3-4D 40 | XferDelimiter (8): 03-00-00-00 41 | 42 | Job Delimiter (32) 43 | 1: 93-36-20-35-A0-0C-10-4A-84-F3-B1-7E-7B-49-9C-D7 44 | 2: 10-13-70-C8-36-53-B3-41-83-E5-81-55-7F-36-1B-87 45 | 3: 8C-93-EA-64-03-0F-68-40-B4-6F-F9-7F-E5-1D-4D-CD 46 | 4: B3-46-ED-3D-3B-10-F9-44-BC-2F-E8-37-8B-D3-19-86 47 | #> 48 | 49 | [CmdletBinding()] 50 | Param( 51 | [Parameter(Mandatory = $False)][String]$Path="$env:ALLUSERSPROFILE\Microsoft\Network\Downloader", 52 | [Parameter(Mandatory = $False)][Switch]$LiveMode = $True 53 | 54 | ) 55 | 56 | # Set switches 57 | $Verbose = $PSBoundParameters.ContainsKey("Verbose") 58 | If ($PSBoundParameters.ContainsKey("Path")){$LiveMode = $False} 59 | 60 | # Test for Elevated privilege if required 61 | If ($Path -eq "$env:ALLUSERSPROFILE\Microsoft\Network\Downloader"){ 62 | If (!(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))){ 63 | Write-Host -ForegroundColor Red "Exiting Invoke-BitsParser: Elevated privilege required for LiveResponse Mode" 64 | exit 65 | } 66 | } 67 | 68 | # Determine files in scope 69 | if(Test-Path $Path -pathType container){ 70 | $QmgrFiles = (Get-ChildItem $Path -Filter qmgr*.dat).FullName 71 | } 72 | ElseIf(Test-Path $Path -pathType Leaf){ 73 | $QmgrFiles = $Path 74 | } 75 | 76 | 77 | ## Regex setup 78 | # Chunk value for Memory optimised regex increment. Keep >= 2500 79 | $Chunk = 2500 80 | 81 | If ($Chunk -lt 2500){ 82 | Write-Host -ForegroundColor Red "Exiting Invoke-BitsParser: Regex `$Chunk of $Chunk is too low!" 83 | exit 84 | } 85 | 86 | $Header = "13F72BC84099124A9F1A3AAEBD894EEA47445F00A9BDBA449851C47BB6C07ACE" 87 | $XferHeader = "36DA56776F515A43ACAC44A248FFF34D" 88 | 89 | 90 | # Job Delimiter lookup 91 | $JobDelimiterList = @( 92 | "93362035A00C104A84F3B17E7B499CD7", 93 | "101370C83653B34183E581557F361B87", 94 | "8C93EA64030F6840B46FF97FE51D4DCD", 95 | "B346ED3D3B10F944BC2FE8378BD31986" 96 | ) 97 | 98 | 99 | # Job Control lookups 100 | $JobTypes = ("download","upload","upload_reply") 101 | 102 | $JobPriority = ("foreground","high","normal","low") 103 | 104 | $JobState = ("queued","connecting", "transferring","suspended","error","transient_error","transferred","acknowleged","cancelled") 105 | 106 | $JobFlags = ("NULL","BG_NOTIFY_JOB_TRANSFERRED","BG_NOTIFY_JOB_ERROR","BG_NOTIFY_JOB_TRANSFERRED_BG_NOTIFY_JOB_ERROR", 107 | "BG_NOTIFY_DISABLE","BG_NOTIFY_JOB_TRANSFERRED_BG_NOTIFY_DISABLE","BG_NOTIFY_JOB_ERROR_BG_NOTIFY_DISABLE", 108 | "BG_NOTIFY_JOB_TRANSFERRED_BG_NOTIFY_JOB_ERROR_BG_NOTIFY_DISABLE","BG_NOTIFY_JOB_MODIFICATION","NULL","NULL","WU Default","NULL", 109 | "NULL","NULL","NULL","BG_NOTIFY_FILE_TRANSFERRED") 110 | 111 | 112 | ## Main 113 | 114 | ForEach($Path in $QmgrFiles){ 115 | 116 | # Resetting Hex stream, variables and results for each QMgrFile 117 | $Hex = $null 118 | $Position = $null 119 | $Output = @{} 120 | 121 | If ($Verbose){Write-Host -ForegroundColor Cyan "Parsing $Path"} 122 | 123 | # Adding QmanagerFile to a Hex Array 124 | $FileStream = New-Object System.IO.FileStream -ArgumentList ($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) 125 | $BinaryReader = New-Object System.IO.BinaryReader $FileStream 126 | $Hex = [System.BitConverter]::ToString($BinaryReader.ReadBytes($FileStream.Length)) -replace "-","" 127 | 128 | # Dispose FileStreams 129 | $FileStream.Dispose() 130 | $BinaryReader.Dispose() 131 | [gc]::Collect() 132 | 133 | 134 | # Finding Headerbytes different position accross some Windows versions using memory optimised regex 135 | $HeaderHit=$null 136 | $a = 0 137 | $b = $Chunk 138 | $Length = $hex.Length 139 | 140 | #$HeaderHit = [regex]::Match($Hex,$Header) 141 | While ($a -lt $Length -and !($HeaderHit)) { 142 | $HeaderHit = [regex]::Match($Hex[$a..$b]-join"",$Header) 143 | [gc]::Collect() 144 | # Add Chunk and allow for usecase of match end of previous block 145 | $a = $a + $Chunk -63 146 | $b = $b + $Chunk -63 147 | } 148 | 149 | If (!$HeaderHit){ 150 | Write-Host -ForegroundColor Cyan "No BITS QueueManager header found." 151 | Break 152 | } 153 | 154 | $Position = $HeaderHit.Index + $HeaderHit.Length 155 | 156 | 157 | # Calculating number of jobs from 2 job bytes (with Endian black magic) 158 | $Jobs=$null 159 | $Jobs = $Hex[$Position..($Position + 7)] 160 | $Jobs = $Jobs[6..7] + $Jobs[4..5] + $Jobs[2..3] + $Jobs[0..1] 161 | 162 | Try{$JobsDetected = [convert]::toint32($Jobs -join "",16)} 163 | Catch{$JobsDetected = "ERROR in"} 164 | 165 | $Position = $Position + 8 166 | 167 | If ($Verbose){Write-Host -ForegroundColor Cyan "$JobsDetected Jobs Detected"} 168 | 169 | 170 | # Determining Job delimiter 171 | $JobDelimiter = $Null 172 | $Match = $Null 173 | $JobDelimiter = $Hex[$Position..($Position + 31)] -join "" 174 | [gc]::Collect() 175 | 176 | Foreach($Item in $JobDelimiterList){ 177 | if ($Item -eq $JobDelimiter){ 178 | $Match = $true 179 | If ($Verbose){Write-Host -ForegroundColor Cyan "Job Delimiter is $JobDelimiter`n"} 180 | } 181 | } 182 | 183 | if (!$Match){ 184 | Write-Host -ForegroundColor Cyan "Could not find Job Delimiter." 185 | break 186 | } 187 | 188 | 189 | For($Job = 0; $Job -lt $JobsDetected; $Job++){ 190 | 191 | # Confirming JobDelimiter next 4 bytes and bumping to next JobDelimiter if required 192 | If($Job -eq 0){ 193 | $Position = $Position + 32 194 | } 195 | Else{ 196 | # Finding $JobDelimiterResults using memory optimised regex 197 | $JobDelimiterResults = $Null 198 | $a = $Position 199 | $b = $Position + $Chunk 200 | 201 | While ($a -lt $Length -and $JobDelimiterResults.count -lt 2){ 202 | [gc]::Collect() 203 | $JobDelimiterResults = $JobDelimiterResults + [regex]::Matches(($Hex[$a..$b] -join ""),$JobDelimiter) 204 | # JobDelimiter minus 1 to allow for usecase of match end of previous block 205 | $a = $a + $Chunk -31 206 | $b = $b + $Chunk -31 207 | if ($JobDelimiterResults.count -eq 2){break} 208 | } 209 | [gc]::Collect() 210 | 211 | $Position = $Position + $JobDelimiterResults[1].Index + $JobDelimiterResults[1].Length 212 | 213 | # Cleaning up 214 | $JobDelimiterResults = $Null 215 | } 216 | 217 | 218 | # Job Type from 2 job bytes (with Endian black magic) 219 | $Type = $null 220 | $Type = $Hex[$Position..($Position + 7)] -join "" 221 | $Type = $Type[6..7] + $Type[4..5] + $Type[2..3] + $Type[0..1] 222 | 223 | Try{$Type = [convert]::toint32($Type -join "",16)} 224 | Catch{$Type = $Null} 225 | 226 | $Position = $Position + 8 227 | 228 | 229 | # Job priority from 2 priority bytes (with Endian black magic) 230 | $Priority = $null 231 | $Priority = $Hex[$Position..($Position + 7)] -join "" 232 | $Priority = $Priority[6..7] + $Priority[4..5] + $Priority[2..3] + $Priority[0..1] 233 | 234 | Try{$Priority = [convert]::toint32($Priority -join "",16)} 235 | Catch{$Priority = $Null} 236 | 237 | $Position = $Position + 8 238 | 239 | 240 | # Job state from 2 state bytes (with Endian black magic) 241 | $State = $null 242 | $State = $Hex[$Position..($Position + 7)] -join "" 243 | $State = $State[6..7] + $State[4..5] + $State[2..3] + $State[0..1] 244 | 245 | Try{$State = [convert]::toint32($State -join "",16)} 246 | Catch{$State = $Null} 247 | 248 | $Position = $Position + 8 249 | 250 | 251 | # Dropping 2 additional bytes 252 | $Position = $Position + 8 253 | 254 | 255 | # Job GUID 1 (with Endian black magic ) 256 | $Guid = $Null 257 | $Guid = $Hex[$Position..($Position + 31)] -join "" 258 | 259 | $Guid = $Guid.substring(6,2) + $Guid.substring(4,2) + $Guid.substring(2,2) + $Guid.substring(0,2) + "-"` 260 | + $Guid.substring(10,2) + $Guid.substring(8,2) + "-" + $Guid.substring(14,2) + $Guid.substring(12,2)` 261 | + "-" + $Guid.substring(16,4) + "-" + $Guid.substring(20,12) 262 | 263 | $Position = $Position + 32 264 | 265 | 266 | # Job NameLength from 2 bytes (with Endian black magic) 267 | $NameLength = $Null 268 | $NameLength = $Hex[$Position..($Position + 7)] -join "" 269 | $NameLength = $NameLength[6..7] + $NameLength[4..5] + $NameLength[2..3] + $NameLength[0..1] 270 | 271 | Try{$NameLength = [convert]::toint32($NameLength -join "",16)} 272 | Catch{$NameLength = $Null} 273 | 274 | $Position = $Position + 8 275 | 276 | 277 | # Parsing each character from Name from 1 bytes x $NameLenth characters 278 | $Name = $Null 279 | 280 | For($Count = 0;$Count -lt ($NameLength -1);$Count++){ 281 | 282 | Try{$Name = $Name + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 283 | Catch{Continue} 284 | 285 | $Position = $Position + 4 286 | } 287 | 288 | $Position = $Position + 4 289 | 290 | 291 | # DescriptioneLength from 2 bytes (with Endian black magic) 292 | $DescriptionLength = $Null 293 | $DescriptionLength = $Hex[$Position..($Position + 7)] -join "" 294 | $DescriptionLength = $DescriptionLength[6..7] + $DescriptionLength[4..5] + $DescriptionLength[2..3] + $DescriptionLength[0..1] 295 | 296 | Try{$DescriptionLength = [convert]::toint32($DescriptionLength -join "",16)} 297 | Catch{$DescriptionLength = $Null} 298 | 299 | $Position = $Position + 8 300 | 301 | 302 | # Parsing each character from Descriptione from 1 bytes x $DescriptionLenth characters 303 | $Description = $Null 304 | 305 | For($Count = 0;$Count -lt ($DescriptionLength -1);$Count++){ 306 | 307 | Try{$Description = $Description + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 308 | Catch{Continue} 309 | 310 | $Position = $Position + 4 311 | } 312 | 313 | $Position = $Position + 4 314 | 315 | 316 | # CommandLineLength from 2 bytes (with Endian black magic) 317 | $CommandLineLength = $Null 318 | $CommandLineLength = $Hex[$Position..($Position + 7)] -join "" 319 | $CommandLineLength = $CommandLineLength[6..7] + $CommandLineLength[4..5] + $CommandLineLength[2..3] + $CommandLineLength[0..1] 320 | 321 | Try{$CommandLineLength = [convert]::toint32($CommandLineLength -join "",16)} 322 | Catch{$CommandLineLength = $null} 323 | 324 | $Position = $Position + 8 325 | 326 | 327 | # Parsing each character from CommandLine from 1 bytes x $CommandLineLenth characters 328 | $CommandLine = $Null 329 | 330 | For($Count = 0;$Count -lt ($CommandLineLength -1);$Count++){ 331 | 332 | Try{$CommandLine = $CommandLine + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 333 | Catch{Continue} 334 | 335 | $Position = $Position + 4 336 | } 337 | 338 | $Position = $Position + 4 339 | 340 | 341 | # CommandLineArgumentsLength from 2 bytes (with Endian black magic) 342 | $CommandLineArgumentsLength = $Null 343 | $CommandLineArgumentsLength = $Hex[$Position..($Position + 7)] -join "" 344 | $CommandLineArgumentsLength = $CommandLineArgumentsLength[6..7] + $CommandLineArgumentsLength[4..5] + $CommandLineArgumentsLength[2..3] + $CommandLineArgumentsLength[0..1] 345 | 346 | Try{$CommandLineArgumentsLength = [convert]::toint32($CommandLineArgumentsLength -join "",16)} 347 | Catch{$CommandLineArgumentsLength = $Null} 348 | 349 | $Position = $Position + 8 350 | 351 | # Parsing each character from CommandLineArguments from 1 bytes x $CommandLineArgumentsLenth characters 352 | $CommandLineArguments = $Null 353 | 354 | For($Count = 0;$Count -lt ($CommandLineArgumentsLength -1);$Count++){ 355 | 356 | Try{$CommandLineArguments = $CommandLineArguments + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 357 | Catch{continue} 358 | 359 | $Position = $Position + 4 360 | } 361 | 362 | $Position = $Position + 4 363 | 364 | 365 | # SidLength from 2 bytes (with Endian black magic) 366 | $SidLength = $Null 367 | $SidLength = $Hex[$Position..($Position + 7)] -join "" 368 | $SidLength = $SidLength[6..7] + $SidLength[4..5] + $SidLength[2..3] + $SidLength[0..1] 369 | 370 | Try{$SidLength = [convert]::toint32($SidLength -join "",16)} 371 | Catch{$SidLength = $Null} 372 | 373 | $Position = $Position + 8 374 | 375 | 376 | # Parsing each character of SID from 1 bytes x $SidLenth characters 377 | $Sid = $Null 378 | 379 | For($Count = 0;$Count -lt ($SidLength -1);$Count++){ 380 | 381 | Try{$Sid = $Sid + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint16($_,16))})} 382 | Catch{continue} 383 | 384 | $Position = $Position + 4 385 | } 386 | 387 | $Position = $Position + 4 388 | 389 | 390 | # Flag from 2 bytes (with Endian black magic) 391 | $Flag = $Null 392 | $Flag = $Hex[$Position..($Position + 7)] -join "" 393 | $Flag = $Flag[6..7] + $Flag[4..5] + $Flag[2..3] + $Flag[0..1] 394 | 395 | Try{$Flag = [convert]::toint32($Flag -join "",16)} 396 | Catch{$Flag=$Null} 397 | 398 | $Position = $Position + 8 399 | 400 | 401 | # Bumping forward to XferHeader using memory optimised regex 402 | $XferHeaderHit = $Null 403 | $a = $Position 404 | $b = $Position + $Chunk 405 | 406 | While ($a -lt $Hex.Length){ 407 | [gc]::Collect() 408 | $XferHeaderHit = [regex]::Match(($Hex[$a..$b] -join ""),$XferHeader) 409 | 410 | # Keeping position 411 | $Position = $a 412 | 413 | # Adding chunk minus xheader less 1 414 | $a = $a + $Chunk -31 415 | $b = $b + $Chunk -31 416 | 417 | If($XferHeaderHit){break} 418 | } 419 | [gc]::Collect() 420 | 421 | If($XferHeaderHit){ 422 | # Needing to account for Regex starting from $Position previously 423 | $Position = $Position + $XferHeaderHit.index + $XferHeaderHit.length 424 | } 425 | Else{break} # We are done for that job 426 | 427 | 428 | # FilesCount in 1 byte (with Endian black magic) 429 | $FilesCount = $null 430 | $FilesCount = $Hex[$Position..($Position + 7)] -join "" 431 | $FilesCount = $FilesCount[6..7] + $FilesCount[4..5] + $FilesCount[2..3] + $FilesCount[0..1] 432 | 433 | Try{$FilesCount = [convert]::toint16($FilesCount -join "",16)} 434 | Catch{$FileCount = $null} 435 | 436 | $Position = $Position + 8 437 | 438 | 439 | 440 | # Some Bits versions will have Jobs with no filecount and requre carving. Jumping for now. 441 | If($FilesCount -gt 0){ 442 | 443 | 444 | # DestLength in 2 bytes (With Endian black magic) 445 | $DestLength = $Null 446 | $DestLength = $Hex[$Position..($Position + 7)] -join "" 447 | $DestLength = $DestLength[6..7] + $DestLength[4..5] + $DestLength[2..3] + $DestLength[0..1] 448 | 449 | Try{$DestLength = [convert]::toint32($DestLength -join "",16)} 450 | Catch{$DestLength = $Null} 451 | 452 | $Position = $Position + 8 453 | 454 | 455 | # DestPath - Parsing each character of DestPath from 1 bytes x $DestPathLength characters 456 | $DestPath = $Null 457 | 458 | For($Count = 0;$Count -lt ($DestLength -1);$Count++){ 459 | 460 | Try{$DestPath = $DestPath + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 461 | Catch{Continue} 462 | 463 | $Position = $Position + 4 464 | } 465 | 466 | $Position = $Position + 4 467 | 468 | 469 | # SourceLength in 2 bytes (With Endian black magic) 470 | $SourceLength = $Null 471 | $SourceLength = $Hex[$Position..($Position + 7)] -join "" 472 | $SourceLength = $SourceLength[6..7] + $SourceLength[4..5] + $SourceLength[2..3] + $SourceLength[0..1] 473 | 474 | Try{$SourceLength = [convert]::toint32($SourceLength -join "",16)} 475 | Catch{$SourceLength = $Null} 476 | 477 | $Position = $Position + 8 478 | 479 | 480 | # SourcePath - Parsing each character of SourcePath from 1 bytes x SourceLength characters 481 | $SourcePath = $Null 482 | 483 | For($Count = 0;$Count -lt ($SourceLength -1);$Count++){ 484 | 485 | Try{$SourcePath = $SourcePath + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 486 | Catch{Continue} 487 | 488 | $Position = $Position + 4 489 | } 490 | 491 | $Position = $Position + 4 492 | 493 | 494 | # TempLength in 2 bytes (With Endian black magic) 495 | $TempLength = $Null 496 | $TempLength = $Hex[$Position..($Position + 7)] -join "" 497 | $TempLength = $TempLength[6..7] + $TempLength[4..5] + $TempLength[2..3] + $TempLength[0..1] 498 | 499 | Try{$TempLength = [convert]::toint16($TempLength -join "",16)} 500 | Catch{$TempLength = $Null} 501 | 502 | $Position = $Position + 8 503 | 504 | 505 | # TempPath - Parsing each character of TempPath from 1 bytes x $TempLength characters 506 | $TempPath = $Null 507 | For($Count = 0;$Count -lt ($TempLength -1);$Count++){ 508 | 509 | Try{$TempPath = $TempPath + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 510 | Catch{Continue} 511 | 512 | $Position = $Position + 4 513 | } 514 | $Position = $Position + 4 515 | 516 | 517 | # DownloadedBytes in 4 bytes (With Endian black magic) 518 | $DownloadedBytes = $Null 519 | $DownloadedBytes = $Hex[$Position..($Position + 15)] -join "" 520 | $DownloadedBytes = $DownloadedBytes[6..7] + $DownloadedBytes[4..5] + $DownloadedBytes[2..3] + $DownloadedBytes[0..1] 521 | 522 | Try{$DownloadedBytes = [convert]::toint32($DownloadedBytes -join "",16)} 523 | Catch{$DownloadedBytes = $null} 524 | 525 | $Position = $Position + 16 526 | 527 | 528 | # TotalBytes in 4 bytes (With Endian black magic) 529 | $TotalBytes = $Null 530 | $TotalBytes = $Hex[$Position..($Position + 15)] -join "" 531 | $TotalBytes = $TotalBytes[6..7] + $TotalBytes[4..5] + $TotalBytes[2..3] + $TotalBytes[0..1] 532 | 533 | Try{$TotalBytes = [convert]::toint32($TotalBytes -join "",16)} 534 | Catch{$TotalBytes = $Null} 535 | 536 | $Position = $Position + 16 537 | 538 | 539 | # Null half byte 540 | $Position = $Position + 2 541 | 542 | 543 | # DriveLength in 2 bytes (With endian black magic) 544 | $DriveLength = $Null 545 | $DriveLength = $Hex[$Position..($Position + 7)] -join "" 546 | $DriveLength = $DriveLength[6..7] + $DriveLength[4..5] + $DriveLength[2..3] + $DriveLength[0..1] 547 | 548 | Try{$DriveLength = [convert]::toint32($DriveLength -join "",16)} 549 | Catch{$DriveLength = $Null} 550 | 551 | $Position = $Position + 8 552 | 553 | 554 | # Drive path by parsing each character from each byte x $DriveLength characters 555 | $Drive = $Null 556 | For($Count = 0;$Count -lt ($DriveLength -1);$Count++){ 557 | 558 | Try{$Drive = $Drive + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 559 | Catch{$Drive = $Null} 560 | 561 | $Position = $Position + 4 562 | } 563 | $Position = $Position + 4 564 | 565 | 566 | # VolumeLength in 2 bytes (With endian black magic) 567 | $VolumeLength = $Null 568 | $VolumeLength = $Hex[$Position..($Position + 7)] -join "" 569 | $VolumeLength = $VolumeLength[6..7] + $VolumeLength[4..5] + $VolumeLength[2..3] + $VolumeLength[0..1] 570 | 571 | Try{$VolumeLength = [convert]::toint32($VolumeLength -join "",16)} 572 | Catch{$VolumeLength = $Null} 573 | 574 | $Position = $Position + 8 575 | 576 | 577 | # Volume by parsing each character from each byte x $VolumeLength characters 578 | $Volume = $Null 579 | For($Count = 0;$Count -lt ($VolumeLength -1);$Count++){ 580 | 581 | Try{$Volume = $Volume + ($Hex[$Position..($Position + 3)] -join "" -replace "00","" | forEach {[char]([convert]::toint32($_,16))})} 582 | Catch{$Volume = $Null} 583 | 584 | $Position = $Position + 4 585 | } 586 | $Position = $Position + 4 587 | 588 | 589 | # Jumping over next XferHeader footer and then some 590 | # If this is not a static number I will need to search for floating $XferHeader = [regex] "36DA56776F515A43ACAC44A248FFF34D" 591 | $Position = $Position + 194 592 | 593 | 594 | # MinRetryDelay in 2 bytes (With endian black magic) 595 | $MinRetryDelay = $Null 596 | $MinRetryDelay = $Hex[$Position..($Position + 7)] -join "" 597 | $MinRetryDelay = $MinRetryDelay[6..7] + $MinRetryDelay[4..5] + $MinRetryDelay[2..3] + $MinRetryDelay[0..1] 598 | 599 | Try{$MinRetryDelay = [convert]::toint32($MinRetryDelay -join "",16)} 600 | Catch{$MinRetryDelay = $Null} 601 | 602 | $Position = $Position + 8 603 | 604 | 605 | # NoProgressTimeout in 2 bytes (With endian black magic) 606 | $NoProgressTimeout = $Null 607 | $NoProgressTimeout = $Hex[$Position..($Position + 7)] -join "" 608 | $NoProgressTimeout = $NoProgressTimeout[6..7] + $NoProgressTimeout[4..5] + $NoProgressTimeout[2..3] + $NoProgressTimeout[0..1] 609 | 610 | Try{$NoProgressTimeout = [convert]::toint32($NoProgressTimeout -join "",16)} 611 | Catch{$NoProgressTimeout = $Null} 612 | 613 | $Position = $Position + 8 614 | 615 | 616 | # CreationTime in 2 bytes (With endian black magic) 617 | $CreationTime = $Null 618 | $CreationTime = $Hex[$Position..($Position + 15)] -join "" 619 | $CreationTime = $CreationTime[14..15] + $CreationTime[12..13] + $CreationTime[10..11] + $CreationTime[8..9] + $CreationTime[6..7] + $CreationTime[4..5] + $CreationTime[2..3] + $CreationTime[0..1] 620 | 621 | Try{$CreationTime = ([datetime]::FromFileTime([Convert]::ToInt64($CreationTime -join "",16))).ToString("s")} 622 | Catch{$CreationTime = "N/A"} 623 | 624 | $Position = $Position + 16 625 | 626 | 627 | # ModificationTime in 2 bytes (With endian black magic) 628 | $ModificationTime = $Null 629 | $ModificationTime = $Hex[$Position..($Position + 15)] -join "" 630 | $ModificationTime = $ModificationTime[14..15] + $ModificationTime[12..13] + $ModificationTime[10..11] + $ModificationTime[8..9] + $ModificationTime[6..7] + $ModificationTime[4..5] + $ModificationTime[2..3] + $ModificationTime[0..1] 631 | 632 | Try{$ModificationTime = ([datetime]::FromFileTime([Convert]::ToInt64($ModificationTime -join "",16))).ToString("s")} 633 | Catch{$ModificationTime = "N/A"} 634 | 635 | $Position = $Position + 16 636 | 637 | 638 | # OtherTime1 in 2 bytes (With endian black magic) 639 | $OtherTime1 = $Null 640 | $OtherTime1 = $Hex[$Position..($Position + 15)] -join "" 641 | $OtherTime1 = $OtherTime1[14..15] + $OtherTime1[12..13] + $OtherTime1[10..11] + $OtherTime1[8..9] + $OtherTime1[6..7] + $OtherTime1[4..5] + $OtherTime1[2..3] + $OtherTime1[0..1] 642 | 643 | Try{$OtherTime1 = ([datetime]::FromFileTime([Convert]::ToInt64($OtherTime1 -join "",16))).ToString("s")} 644 | Catch{$OtherTime1 = "N/A"} 645 | 646 | $Position = $Position + 16 647 | 648 | 649 | # Jump 7 unknown bytes 650 | $Position = $Position + 28 651 | 652 | 653 | # OtherTime2 in 2 bytes (With endian black magic) 654 | $OtherTime2 = $Null 655 | $OtherTime2 = $Hex[$Position..($Position + 15)] -join "" 656 | $OtherTime2 = $OtherTime2[14..15] + $OtherTime2[12..13] + $OtherTime2[10..11] + $OtherTime2[8..9] + $OtherTime2[6..7] + $OtherTime2[4..5] + $OtherTime2[2..3] + $OtherTime2[0..1] 657 | 658 | Try{$OtherTime2 = ([datetime]::FromFileTime([Convert]::ToInt64($OtherTime1 -join "",16))).ToString("s")} 659 | Catch{$OtherTime2 = "N/A"} 660 | 661 | $Position = $Position + 16 662 | 663 | 664 | # ExpirationTime in 2 bytes (With endian black magic) 665 | $ExpirationTime = $Null 666 | $ExpirationTime = $Hex[$Position..($Position + 15)] -join "" 667 | $ExpirationTime = $ExpirationTime[14..15] + $ExpirationTime[12..13] + $ExpirationTime[10..11] + $ExpirationTime[8..9] + $ExpirationTime[6..7] + $ExpirationTime[4..5] + $ExpirationTime[2..3] + $ExpirationTime[0..1] 668 | 669 | Try{$ExpirationTime = ([datetime]::FromFileTime([Convert]::ToInt64($ExpirationTime -join "",16))).ToString("s")} 670 | Catch{$ExpirationTime = "N/A"} 671 | 672 | $Position = $Position + 16 673 | 674 | } 675 | Else{ 676 | # Setting skipped variables to Null for results. 677 | $SourcePath = $null 678 | $DestPath = $null 679 | $TempPath = $null 680 | $MinRetryDelay = $null 681 | $NoProgressTimeout = $null 682 | $CreationTime = $null 683 | $ModificationTime = $null 684 | $OtherTime1 = $null 685 | $OtherTime2 = $null 686 | $ExpirationTime = $null 687 | 688 | } 689 | 690 | 691 | # Display Results 692 | $Output = [ordered] @{ 693 | "Data File" = $Path 694 | "Job Guid" = $Guid 695 | "Job Name" = $Name 696 | Description = $Description 697 | Type = $JobTypes[$Type] 698 | Priority = $JobPriority[$Priority] 699 | JobState = $JobState[$State] 700 | CommandLine = $CommandLine 701 | Arguments = $CommandLineArguments 702 | SID = $Sid 703 | Flags = $JobFlags[$Flag] 704 | Files = $FilesCount 705 | Source = $SourcePath 706 | "Temp Path" = $TempPath 707 | Destination = $DestPath 708 | "Downloaded Bytes" = $DownloadedBytes 709 | "Total Bytes" = $TotalBytes 710 | Drive = $Drive 711 | Volume = $Volume 712 | "Minimun Retry Delay" = $MinRetryDelay 713 | "No Progress Timeout" = $NoProgressTimeout 714 | "Creation Time" = $CreationTime 715 | "Modification Time" = $ModificationTime 716 | "Other time 1" = $OtherTime1 717 | "Other time 2" = $OtherTime2 718 | "Expiration Time" = $ExpirationTime 719 | } 720 | 721 | $Output 722 | "`n" 723 | [gc]::Collect() 724 | } 725 | } --------------------------------------------------------------------------------