-
Notifications
You must be signed in to change notification settings - Fork 2
/
terminal_sync.psm1
253 lines (217 loc) · 10.8 KB
/
terminal_sync.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# =============================================================================
# ****** Configuration Settings ******
# =============================================================================
# The name / identifier of the user creating the log entries
$env:OPERATOR = ""
# The IP and port where the terminal_sync server is running
$global:TermSyncServer = "127.0.0.1:8000"
# Enable / disable terminal_sync logging at runtime
$global:TermSyncLogging = $true
# Controls the verbosity of the terminal_sync console output
# 0 (None): No terminal_sync output will be displayed
# 1 (ExecOnly): Only the executed command and timestamps will be displayed
# 2 (SuccessOnly): terminal_sync will display a message on logging success
# 3 (IgnoreTermSyncConnError): Only errors contacting the terminal_sync server will be suppressed
# 4 (All): All terminal_sync output will be displayed
$global:TermSyncVerbosity = 4
# The number of seconds the client will wait for a response from the terminal_sync server
$global:TimeoutSec = 4
# =============================================================================
# ****** Management Functions ******
# =============================================================================
function Enable-TermSync {
$global:TermSyncLogging = $true
Write-Host "[+] terminal_sync logging enabled"
}
function Disable-TermSync {
$global:TermSyncLogging = $False
Write-Host "[+] terminal_sync logging disabled"
}
function Set-TermSyncVerbosity {
# Use the DisplayLevel enum as the parameter type to allow tab completion
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[DisplayLevel]$Level
)
$global:TermSyncVerbosity = $Level
Write-Host "[+] terminal_sync log level set to: $Level"
}
# =============================================================================
# ****** Terminal Sync Client ******
# =============================================================================
# Define the console verbosity levels
Add-Type -TypeDefinition @"
public enum DisplayLevel
{
None = 0,
ExecOnly = 1,
SuccessOnly = 2,
IgnoreTermSyncConnError = 3,
All = 4
}
"@
# Enumerated type to track the log status of each command
Add-Type -TypeDefinition @"
public enum LogStatus
{
Logged,
Completed
}
"@
# Dictionary used to track which commands have be logged and successfully updated
$global:CommandTracker = @{}
# Initialize the command index so our command count aligns with the Id attribute in PowerShell's history
# Add 1 because the command to load this module (e.g., `Import-Module terminal_sync.psm1`) will increment the history
$global:CommandIndex = $(Get-History -Count 1).Id + 1
# Pre-exec hook
function PSConsoleHostReadLine {
# Prompt the user for a command line to submit, save it in a variable and
# pass it through, by enclosing it in (...)
$Command = [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($Host.Runspace, $ExecutionContext)
if ($TermSyncLogging) {
$StartTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")
try {
# Only react to non-blank lines
if ($command.Trim()) {
# Increment command index so it will match `$(Get-History -Count 1).Id` for this command
$global:CommandIndex += 1
if ($TermSyncVerbosity -gt [DisplayLevel]::None) {
Write-Host "[*] Executed: `"$Command`" at $StartTime"
}
$Params = @{
Method = "Post"
Uri = "http://$TermSyncServer/commands/"
ContentType = "application/json"
TimeoutSec = $TimeoutSec
Body = (@{
uuid = "$($Host.InstanceId).$global:CommandIndex"
command = $Command
start_time = $StartTime
source_host = $env:SRC_HOST
comments = "PowerShell Session: $($Host.InstanceId)"
operator = $env:OPERATOR
} | ConvertTo-Json
)
}
try {
$Response = Invoke-RestMethod @Params
# If the request is successful, the response will be a string to be displayed to the user
# If the command does not trigger logging, an HTTP 204 with no body is returned; don't print anything
if ($Response) {
if ($TermSyncVerbosity -gt [DisplayLevel]::ExecOnly) {
Write-Host $Response
}
# Store the status of the command
$global:CommandTracker.Set_Item($global:CommandIndex.ToString(), [LogStatus]::Logged)
}
}
catch {
# Get the HTTP status code
$StatusCode = $_.Exception.Response.StatusCode.value__
# An error occurred; the server will return a JSON object with a "detail" attribute
# (e.g., '{"detail":"An error occurred while trying to log to GhostWriter: Cannot connect to host"}')
if ($StatusCode) {
# If an exception occurred server-side; throw an exception with the message from the server
throw $($_ | ConvertFrom-Json).detail
}
elseif ($_.ToString() -eq "Unable to connect to the remote server") {
if ($TermSyncVerbosity -gt [DisplayLevel]::IgnoreTermSyncConnError) {
# Clarify which server could not be contacted
throw "Unable to connect to terminal_sync server"
}
}
else {
# Otherwise, re-raise the exception
throw $_
}
}
}
}
catch {
if ($TermSyncVerbosity -gt [DisplayLevel]::SuccessOnly) {
# Clearly indicate the error is from terminal_sync and not the command the user ran
Write-Host -ForegroundColor Red "[terminal_sync] [ERROR]: $_"
}
}
}
# IMPORTANT: Ensure the original command is returned so it gets executed
$command
}
# Post-exec hook
function Prompt {
if ($TermSyncLogging) {
try {
# Retrieve the last command
$LastCommand = Get-History -Count 1
# If the last command exists and isn't complete, process it
if ($LastCommand -and $global:CommandTracker[$LastCommand.Id.ToString()] -ne [LogStatus]::Completed) {
# Convert the start and end timestamps to properly formatted strings (in UTC)
$StartTime = $LastCommand.StartExecutionTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")
$EndTime = $LastCommand.EndExecutionTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")
if ($TermSyncVerbosity -gt [DisplayLevel]::None) {
Write-Host "[+] Completed: `"$($LastCommand.CommandLine)`" at $EndTime"
}
$Params = @{
Method = "Put"
Uri = "http://$TermSyncServer/commands/"
ContentType = "application/json"
TimeoutSec = $TimeoutSec
Body = (@{
# Use the PowerShell session ID and the index of the command to uniquely identify it
uuid = "$($Host.InstanceId).$($LastCommand.Id)"
command = $LastCommand.CommandLine
start_time = $StartTime
end_time = $EndTime
source_host = $env:SRC_HOST
# Set output to the execution status of the command
output = if ($?) { "Success" } else { "Failed" }
comments = "PowerShell Session: $($Host.InstanceId)"
operator = $env:OPERATOR
} | ConvertTo-Json
)
}
try {
$Response = Invoke-RestMethod @Params
# If the request is successful, the response will be a string to be displayed to the user
# If the command does not trigger logging, an HTTP 204 with no body is returned; don't print anything
if ($Response) {
if ($TermSyncVerbosity -gt [DisplayLevel]::ExecOnly) {
Write-Host $Response
}
# Mark the command as completed so we don't get a duplicate if the user presses Enter on an empty line
$global:CommandTracker.Set_Item($LastCommand.Id.ToString(), [LogStatus]::Completed)
}
}
catch {
# Get the HTTP status code
$StatusCode = $_.Exception.Response.StatusCode.value__
# An error occurred; the server will return a JSON object with a "detail" attribute
# (e.g., '{"detail":"An error occurred while trying to log to GhostWriter: Cannot connect to host"}')
if ($StatusCode) {
# If an exception occurred server-side; throw an exception with the message from the server
throw $($_ | ConvertFrom-Json).detail
}
elseif ($_.ToString() -eq "Unable to connect to the remote server") {
if ($TermSyncVerbosity -gt [DisplayLevel]::IgnoreTermSyncConnError) {
# Clarify which server could not be contacted
throw "Unable to connect to terminal_sync server"
}
}
else {
# Otherwise, re-raise the exception
throw $_
}
}
}
}
catch {
if ($TermSyncVerbosity -gt [DisplayLevel]::SuccessOnly) {
# Clearly indicate the error is from terminal_sync and not the command the user ran
Write-Host -ForegroundColor Red "[terminal_sync] [ERROR]: $_"
}
}
}
# Return the PowerShell prompt
"PS $($executionContext.SessionState.Path.CurrentLocation.Path)$('>' * ($nestedPromptLevel + 1)) "
}