Adding Python extensions support for Windows#48
Conversation
fmanco
left a comment
There was a problem hiding this comment.
Generally I like the code structure. I left a few comments but I should let you know that I'm not familiar with Windows or the win32 API so they're more generic.
osquery/TPipe.py
Outdated
| raise TTransportException(TTransportException.ALREADY_OPEN) | ||
|
|
||
| h = None | ||
| while True: |
There was a problem hiding this comment.
I'm not familiar with this APIs (and Windows in general) but should we have something like a max_retries here to avoid this getting stuck forever on winerror.ERROR_PIPE_BUSY? It's probably an unlikely scenario but still.
osquery/TPipe.py
Outdated
| raise TTransportException( | ||
| type=TTransportException.UNKNOWN, message='TPipe read failed') | ||
|
|
||
| if (err != 0): |
| type=TTransportException.UNKNOWN, | ||
| message='Failed to write to named pipe: ' + e.message) | ||
|
|
||
| def flush(self): |
osquery/TPipe.py
Outdated
|
|
||
| bytesWritten = None | ||
| try: | ||
| (writeError, bytesWritten) = win32file.WriteFile( |
There was a problem hiding this comment.
Is ignoring writeError intentional? If so maybe should add a comment. We should also handle bytesWritten. How would the caller know if the full buffer was written?
osquery/TPipe.py
Outdated
|
|
||
| # Create a new named pipe if one doesn't already exist | ||
| def listen(self): | ||
| if self._handle == None: |
There was a problem hiding this comment.
Doesn't make much difference here but should use is instead of == as a rule.
osquery/TPipe.py
Outdated
| raise TTransportException( | ||
| type=TTransportException.NOT_OPEN, | ||
| message='TCreateNamedPipe failed: {}'.format(err)) | ||
| return False |
There was a problem hiding this comment.
I think this is unreachable as you're raising an exception before. Also for consistency maybe it makes sense to throw on error and not return as you're doing on other methods.
osquery/TPipe.py
Outdated
|
|
||
| def accept(self): | ||
| if self._handle == None: | ||
| self.createNamedPipe() |
There was a problem hiding this comment.
Does it make sense to allow an accept before having created a pipe? Maybe this should just fail if self._handle is None.
There was a problem hiding this comment.
That definitely makes sense to me, this logic flow was modeled after how the logic happens in Apache thrifts C++ implementation, so my guess is that maybe this happens occasionally? I'd prefer to leave it to stay consistent with the C++ impl, but am open to a discussion about it.
osquery/TPipe.py
Outdated
| message='TCreateNamedPipe failed') | ||
|
|
||
| def accept(self): | ||
| if self._handle == None: |
|
|
||
| if ret == winerror.ERROR_PIPE_CONNECTED: | ||
| win32event.SetEvent(self._overlapped.hEvent) | ||
| break |
There was a problem hiding this comment.
I think we actually want the same logic as above with the client here. The intent was we'd have another while True loop that continues to attempt the connection, but this definitely needs cleaning!
osquery/management.py
Outdated
| self._socket = tempfile.mkstemp(prefix="pyosqsock") | ||
| self._pidfile = tempfile.mkstemp(prefix="pyosqpid") | ||
| with open(self._pidfile[1], "w") as fh: | ||
| fh.write("100000") |
There was a problem hiding this comment.
I think you forgot to actually get the PID. Also maybe we should handle errors more gracefully here than allowing IOError to propagate.
There was a problem hiding this comment.
I think this was testing logic that I forgot to remove, going to just nuke this component, as I think on posix we don't need to worry about the pidfile.
35c6988 to
9e2dceb
Compare
9e2dceb to
1931549
Compare
|
Huh, not entirely sure what I'm doing to screw up tests, however funny thing - tests pass on Windows :D |
|
Looks like the only failing test is due to Python 2.6 no longer being supported: |
| Generic close method, as both server and client rely on closing pipes | ||
| in the same way | ||
| """ | ||
| if self._handle is not None: |
There was a problem hiding this comment.
Isn't it always better to specifically test for None when that's what you care about? In this case if not self._handle will call __eq__ that can be overridden by the object I think...
There was a problem hiding this comment.
I'm inclined to agree with @fmanco on this one, but that's largely because I'm more keen on explicit instead of implicit comparisons :\
osquery/TPipe.py
Outdated
| while conns < self._maxConnAttempts: | ||
| try: | ||
| h = win32file.CreateFile( | ||
| self._pipeName, |
There was a problem hiding this comment.
It's hard to read. Please indent 4 spaces
osquery/TPipe.py
Outdated
| try: | ||
| h = win32file.CreateFile( | ||
| self._pipeName, | ||
| win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, None, |
| 'Failed to open connection to pipe: {}'.format(e)) | ||
|
|
||
| # Successfully connected, break out. | ||
| if h is not None and h.handle != winerror.ERROR_INVALID_HANDLE: |
There was a problem hiding this comment.
it's fine, nit it's better to check the type of the structure you look for in h.
This also could be if not h and h.handle ...
osquery/TPipe.py
Outdated
| raise TTransportException( | ||
| type=TTransportException.UNKNOWN, message='TPipe read failed') | ||
|
|
||
| if err != 0: |
osquery/extension_client.py
Outdated
| from osquery.extensions.ExtensionManager import Client | ||
|
|
||
| DEFAULT_SOCKET_PATH = "/var/osquery/osquery.em" | ||
| if sys.platform == "win32": |
There was a problem hiding this comment.
"win32" should be a const in thrift spec?
There was a problem hiding this comment.
or a const somewhere we can resue?
osquery/extension_client.py
Outdated
| DEFAULT_SOCKET_PATH = r'\\.\pipe\osquery.em' | ||
| else: | ||
| DEFAULT_SOCKET_PATH = "/var/osquery/osquery.em" | ||
| """The default path for osqueryd sockets""" |
osquery/extension_client.py
Outdated
| sock = TPipe(pipeName=self.path) | ||
| else: | ||
| if uuid: | ||
| self.path += ".%s" % str(uuid) |
There was a problem hiding this comment.
could be:
self.path += ".{}".format(uuid) if uuid else ""
osquery/management.py
Outdated
| from thrift.transport import TSocket | ||
| from thrift.transport import TTransport | ||
|
|
||
| if sys.platform == "win32": |
There was a problem hiding this comment.
"win32" is using a lot. plz consider make it a const or part of thrift spec
osquery/management.py
Outdated
|
|
||
| transport = None | ||
| if sys.platform == 'win32': | ||
| transport = TPipeServer(args.socket + "." + str(status.uuid)) |
There was a problem hiding this comment.
"{}.{}".format(args.socket, status.uuid)
9065183 to
3ccf6b1
Compare
|
With the most recent commit I've verified this does not impact python extensions on posix platforms. |
osquery/TPipe.py
Outdated
| saAttr = pywintypes.SECURITY_ATTRIBUTES() | ||
| saAttr.SetSecurityDescriptorDacl(1, None, 0) | ||
|
|
||
| self._handle = win32pipe.create_named_pipe( |
There was a problem hiding this comment.
This causes an error since create_named_pipe doesn't exist in the version of pywin32 I have.
Changing it to win32pipe.CreateNamedPipe fixed it.
There was a problem hiding this comment.
Note this was using the latest pypiwin32.
osquery/TPipe.py
Outdated
| except Exception as e: | ||
| raise TTransportException( | ||
| type=TTransportException.UNKNOWN, | ||
| message='Failed to write to named pipe: ' + e.message) |
There was a problem hiding this comment.
There was no e.message when I tested this, might want to use str(e)
| raise TTransportException( | ||
| type=TTransportException.NOT_OPEN, | ||
| message='TConnectNamedPipe failed: {}'.format(e.message)) | ||
|
|
|
I found a few bugs, see above comments. With those fixed I'd be good to merge, was able to get this working successfully once I made the changes locally. |
osquery/TPipe.py
Outdated
| win32file.FILE_FLAG_OVERLAPPED, | ||
| None) | ||
| except Exception as e: | ||
| if e[0] != winerror.ERROR_PIPE_BUSY: |
There was a problem hiding this comment.
This line gives an IndexError - I think it should just be 'if e 1= ..'.
Traceback (most recent call last):
File "C:\Python36-32\lib\site-packages\osquery-3.0.1-py3.6.egg\osquery\TPipe.py", line 76, in open
pywintypes.error: (2, 'CreateFile', 'The system cannot find the file specified.')
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\ProgramData\osquery\extensions\bb_e2e_test.ext", line 69, in <module>
osquery.start_extension(name="bb_e2e_tests", version="1.0.0")
File "C:\Python36-32\lib\site-packages\osquery-3.0.1-py3.6.egg\osquery\management.py", line 198, in start_extension
File "C:\Python36-32\lib\site-packages\osquery-3.0.1-py3.6.egg\osquery\extension_client.py", line 62, in open
File "C:\Python36-32\lib\site-packages\thrift\transport\TTransport.py", line 153, in open
return self.__trans.open()
File "C:\Python36-32\lib\site-packages\osquery-3.0.1-py3.6.egg\osquery\TPipe.py", line 78, in open
TypeError: 'error' object does not support indexing
There was a problem hiding this comment.
Ok will take a look. Just as a heads up though, looks like you’re testing this with Python 3,m given The path to your binary, and I’m honestly not sure how this code will behave. That index does look correct to me though, as e[0] will be the error code which is what we are intending to check against, but I’ll see if I can sort out what’s up with the exception. Thanks for testing!
There was a problem hiding this comment.
Ah, good catch. In fact, awesome to know this works out of the box in py3 :).
|
I'm good with these changes, code looks good and testing proved successful. I am seeing some stability issues but need further testing and am OK with this being shipped in current state. |
|
One note, which i can file as a seperate issue if you prefer, is that when using osqueryi to call an extension table, it works the first time but then doesn't work until i kill and restart osqueryi. After running the select * from an extension table in osqueryi, after the first time all subsequent runs just return nothing until i restart a new osqueryi process. |
|
BTW some of the stability issues I saw look similar to #3954 on osquery, so this likely isn't related to this diff but a generic windows extensions issue. |
|
👍 |
|
We can remove build for 2.6 in a follow up PR, do not let that stop you from merging this. |
|
I'm going to move forward with landing this as in it's current form this is working on Windows with osquery running as a system level service, and a configuration that queries against a python extensions table. Any remaining issues that folks have for this can be filed as follow up issues and I'll deal with them :) |
There was a problem hiding this comment.
Getting error while running on Windows , I guess it should use AF_INET
Traceback (most recent call last):
File "run.py", line 14, in
CLIENT.open()
File "C:\Python27\lib\site-packages\osquery\extension_client.py", line 51, in open
self._transport.open()
File "C:\Python27\lib\site-packages\thrift\transport\TTransport.py", line 153, in open
return self.__trans.open()
File "C:\Python27\lib\site-packages\thrift\transport\TSocket.py", line 95, in open
addrs = self._resolveAddr()
File "C:\Python27\lib\site-packages\thrift\transport\TSocket.py", line 34, in _resolveAddr
return [(socket.AF_UNIX, socket.SOCK_STREAM, None, None,
AttributeError: 'module' object has no attribute 'AF_UNIX'
|
@SudhirSingh20 you’ll wanna check what version of the osquery python module you’ve got installed, as that looks like the old logic. Ensure you’re running off of 3.0.2, and if you have more problems feel free to open an issue |
This adds in an implementation of TPipe for windows, which allows us to make use of Python extensions on the Windows platform.
Some samples of the extensions in action. First we startup osquery:
Then we start up our extension:
We see the extension connect to our shell:
We then query the extension:
So far I have tested extensions being autloaded with osquery, which currently works from both command line and running as a system service. I tested having a python extension that both provides data to, and queries data from osquery, and this didn't seem to work. I'm still digging into what's happening to prevent bidirectional communication, but throwing this up to get the review process started.