panopticon-cli 1.1.2__tar.gz → 1.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {panopticon_cli-1.1.2/panopticon_cli.egg-info → panopticon_cli-1.1.5}/PKG-INFO +1 -1
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/cli.py +161 -18
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5/panopticon_cli.egg-info}/PKG-INFO +1 -1
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/pyproject.toml +1 -1
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_cli.py +7 -6
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/LICENSE +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/README.md +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/__init__.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/memory.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/observer.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/policies.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/sentinel.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/SOURCES.txt +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/dependency_links.txt +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/entry_points.txt +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/requires.txt +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/top_level.txt +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/setup.cfg +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_memory.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_observer.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_policies.py +0 -0
- {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_sentinel.py +0 -0
|
@@ -32,11 +32,12 @@ class CLIWrapper:
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
self.process = None
|
|
35
|
+
self.master_fd = None
|
|
35
36
|
self.buffer = []
|
|
36
37
|
self.running = False
|
|
37
38
|
self.lock = threading.Lock()
|
|
38
39
|
self.threads = []
|
|
39
|
-
self.
|
|
40
|
+
self._stop_event = threading.Event()
|
|
40
41
|
|
|
41
42
|
def run(self):
|
|
42
43
|
self.running = True
|
|
@@ -51,15 +52,46 @@ class CLIWrapper:
|
|
|
51
52
|
f"AdversarialLogic"
|
|
52
53
|
)
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stdin=subprocess.PIPE,
|
|
60
|
-
bufsize=0,
|
|
61
|
-
env=env,
|
|
55
|
+
use_pty = (
|
|
56
|
+
sys.platform != "win32"
|
|
57
|
+
and sys.stdin is not None
|
|
58
|
+
and sys.stdin.isatty()
|
|
59
|
+
and sys.stdout.isatty()
|
|
62
60
|
)
|
|
61
|
+
self.master_fd = None
|
|
62
|
+
|
|
63
|
+
if use_pty:
|
|
64
|
+
import pty
|
|
65
|
+
|
|
66
|
+
master_fd, slave_fd = pty.openpty()
|
|
67
|
+
self.master_fd = master_fd
|
|
68
|
+
self.process = subprocess.Popen(
|
|
69
|
+
self.command,
|
|
70
|
+
stdout=slave_fd,
|
|
71
|
+
stderr=slave_fd,
|
|
72
|
+
stdin=slave_fd,
|
|
73
|
+
bufsize=0,
|
|
74
|
+
env=env,
|
|
75
|
+
close_fds=True,
|
|
76
|
+
)
|
|
77
|
+
os.close(slave_fd)
|
|
78
|
+
use_stdin_thread = True
|
|
79
|
+
else:
|
|
80
|
+
stdin_target = subprocess.PIPE
|
|
81
|
+
use_stdin_thread = True
|
|
82
|
+
|
|
83
|
+
if sys.platform != "win32" and sys.stdin is not None and sys.stdin.isatty():
|
|
84
|
+
stdin_target = sys.stdin
|
|
85
|
+
use_stdin_thread = False
|
|
86
|
+
|
|
87
|
+
self.process = subprocess.Popen(
|
|
88
|
+
self.command,
|
|
89
|
+
stdout=subprocess.PIPE,
|
|
90
|
+
stderr=subprocess.STDOUT,
|
|
91
|
+
stdin=stdin_target,
|
|
92
|
+
bufsize=0,
|
|
93
|
+
env=env,
|
|
94
|
+
)
|
|
63
95
|
|
|
64
96
|
t_out = threading.Thread(target=self._read_stdout, daemon=True)
|
|
65
97
|
t_out.start()
|
|
@@ -69,9 +101,10 @@ class CLIWrapper:
|
|
|
69
101
|
t_eval.start()
|
|
70
102
|
self.threads.append(t_eval)
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
if use_stdin_thread:
|
|
105
|
+
t_in = threading.Thread(target=self._read_stdin, daemon=True)
|
|
106
|
+
t_in.start()
|
|
107
|
+
self.threads.append(t_in)
|
|
75
108
|
|
|
76
109
|
# Register cleanup on exit
|
|
77
110
|
atexit.register(self._cleanup)
|
|
@@ -87,6 +120,14 @@ class CLIWrapper:
|
|
|
87
120
|
def _cleanup(self):
|
|
88
121
|
"""Cleanly shutdown all threads and resources."""
|
|
89
122
|
self.running = False
|
|
123
|
+
self._stop_event.set()
|
|
124
|
+
|
|
125
|
+
if getattr(self, "master_fd", None) is not None:
|
|
126
|
+
try:
|
|
127
|
+
os.close(self.master_fd)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
self.master_fd = None
|
|
90
131
|
|
|
91
132
|
# Close subprocess pipes FIRST before thread cleanup
|
|
92
133
|
if self.process:
|
|
@@ -123,7 +164,10 @@ class CLIWrapper:
|
|
|
123
164
|
try:
|
|
124
165
|
while self.running and self.process and self.process.poll() is None:
|
|
125
166
|
try:
|
|
126
|
-
|
|
167
|
+
if getattr(self, "master_fd", None) is not None:
|
|
168
|
+
byte_chunk = os.read(self.master_fd, 1)
|
|
169
|
+
else:
|
|
170
|
+
byte_chunk = self.process.stdout.read(1)
|
|
127
171
|
if not byte_chunk:
|
|
128
172
|
break
|
|
129
173
|
|
|
@@ -142,7 +186,8 @@ class CLIWrapper:
|
|
|
142
186
|
finally:
|
|
143
187
|
try:
|
|
144
188
|
if (
|
|
145
|
-
self
|
|
189
|
+
getattr(self, "master_fd", None) is None
|
|
190
|
+
and self.process
|
|
146
191
|
and self.process.stdout
|
|
147
192
|
and (not self.process.stdout.closed)
|
|
148
193
|
):
|
|
@@ -150,13 +195,109 @@ class CLIWrapper:
|
|
|
150
195
|
except Exception:
|
|
151
196
|
pass
|
|
152
197
|
|
|
198
|
+
def _stdin_ready(self) -> bool:
|
|
199
|
+
if sys.platform == "win32":
|
|
200
|
+
if (
|
|
201
|
+
sys.stdin is not None
|
|
202
|
+
and getattr(sys.stdin, "isatty", lambda: False)()
|
|
203
|
+
and hasattr(sys.stdin, "fileno")
|
|
204
|
+
):
|
|
205
|
+
try:
|
|
206
|
+
import msvcrt
|
|
207
|
+
|
|
208
|
+
return msvcrt.kbhit()
|
|
209
|
+
except ImportError:
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
212
|
+
else:
|
|
213
|
+
import select
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0)
|
|
217
|
+
return bool(rlist)
|
|
218
|
+
except Exception:
|
|
219
|
+
return False
|
|
220
|
+
|
|
153
221
|
def _read_stdin(self):
|
|
222
|
+
if sys.platform == "win32":
|
|
223
|
+
if (
|
|
224
|
+
sys.stdin is not None
|
|
225
|
+
and getattr(sys.stdin, "isatty", lambda: False)()
|
|
226
|
+
and hasattr(sys.stdin, "fileno")
|
|
227
|
+
and sys.stdin.fileno() == 0
|
|
228
|
+
):
|
|
229
|
+
self._read_stdin_windows()
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
stdin_buffer = getattr(sys.stdin, "buffer", None)
|
|
233
|
+
if stdin_buffer is None:
|
|
234
|
+
return
|
|
235
|
+
|
|
154
236
|
try:
|
|
155
|
-
while
|
|
237
|
+
while (
|
|
238
|
+
self.running
|
|
239
|
+
and self.process
|
|
240
|
+
and self.process.poll() is None
|
|
241
|
+
and not self._stop_event.is_set()
|
|
242
|
+
):
|
|
243
|
+
if not self._stdin_ready():
|
|
244
|
+
time.sleep(0.1)
|
|
245
|
+
continue
|
|
246
|
+
|
|
156
247
|
try:
|
|
157
|
-
|
|
248
|
+
read_func = getattr(stdin_buffer, "read1", stdin_buffer.read)
|
|
249
|
+
data = read_func(1024)
|
|
158
250
|
if not data:
|
|
159
251
|
break
|
|
252
|
+
if getattr(self, "master_fd", None) is not None:
|
|
253
|
+
os.write(self.master_fd, data)
|
|
254
|
+
elif self.process.stdin and not self.process.stdin.closed:
|
|
255
|
+
self.process.stdin.write(data)
|
|
256
|
+
self.process.stdin.flush()
|
|
257
|
+
except (OSError, ValueError, BrokenPipeError):
|
|
258
|
+
break
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
finally:
|
|
262
|
+
try:
|
|
263
|
+
if (
|
|
264
|
+
getattr(self, "master_fd", None) is None
|
|
265
|
+
and self.process
|
|
266
|
+
and self.process.stdin
|
|
267
|
+
and (not self.process.stdin.closed)
|
|
268
|
+
):
|
|
269
|
+
self.process.stdin.close()
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def _read_stdin_windows(self):
|
|
274
|
+
try:
|
|
275
|
+
import msvcrt
|
|
276
|
+
except ImportError:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
while (
|
|
281
|
+
self.running
|
|
282
|
+
and self.process
|
|
283
|
+
and self.process.poll() is None
|
|
284
|
+
and not self._stop_event.is_set()
|
|
285
|
+
):
|
|
286
|
+
if not msvcrt.kbhit():
|
|
287
|
+
time.sleep(0.1)
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
char = msvcrt.getwch()
|
|
292
|
+
if char == "\x03":
|
|
293
|
+
if self.process.poll() is None:
|
|
294
|
+
try:
|
|
295
|
+
self.process.send_signal(signal.CTRL_C_EVENT)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
data = char.encode("utf-8")
|
|
160
301
|
if self.process.stdin and not self.process.stdin.closed:
|
|
161
302
|
self.process.stdin.write(data)
|
|
162
303
|
self.process.stdin.flush()
|
|
@@ -216,8 +357,10 @@ class CLIWrapper:
|
|
|
216
357
|
|
|
217
358
|
try:
|
|
218
359
|
# Write the injection payload as binary
|
|
219
|
-
|
|
220
|
-
|
|
360
|
+
payload = (e.course_correction + "\n").encode("utf-8")
|
|
361
|
+
if getattr(self, "master_fd", None) is not None:
|
|
362
|
+
os.write(self.master_fd, payload)
|
|
363
|
+
elif self.process.stdin and not self.process.stdin.closed:
|
|
221
364
|
self.process.stdin.write(payload)
|
|
222
365
|
self.process.stdin.flush()
|
|
223
366
|
except Exception as ex:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import sys
|
|
3
|
+
from unittest.mock import patch
|
|
3
4
|
|
|
4
5
|
from panopticon.cli import CLIWrapper, strip_ansi
|
|
5
6
|
|
|
@@ -52,12 +53,12 @@ def test_read_stdin_forwards_input():
|
|
|
52
53
|
wrapper.running = True
|
|
53
54
|
wrapper.process = DummyProcess()
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
dummy_stdin = DummyStdinStream(b"hello world\n")
|
|
57
|
+
dummy_stdin.isatty = lambda: True
|
|
58
|
+
|
|
59
|
+
with patch("panopticon.cli.sys.stdin", dummy_stdin):
|
|
60
|
+
with patch.object(CLIWrapper, "_stdin_ready", return_value=True):
|
|
61
|
+
wrapper._read_stdin()
|
|
61
62
|
|
|
62
63
|
assert wrapper.process.stdin.written == b"hello world\n"
|
|
63
64
|
assert wrapper.process.stdin.closed is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|