Skip to content

Adding Python extensions support for Windows#48

Merged
muffins merged 24 commits intoosquery:masterfrom
muffins:osquery-python-windows-port
Apr 11, 2018
Merged

Adding Python extensions support for Windows#48
muffins merged 24 commits intoosquery:masterfrom
muffins:osquery-python-windows-port

Conversation

@muffins
Copy link
Contributor

@muffins muffins commented Feb 1, 2018

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:

:\Users\Nick\work\repos\osquery [win-python-auto-ext]
λ  .\build\windows10\osquery\RelWithDebInfo\osqueryd.exe -S --flagfile=C:\Users\Nick\work\configs\osquery_extensions\osquery.flags --verbose

Then we start up our extension:

C:\Users\Nick\work\repos\osquery-python [osquery-python-windows-port +1 ~0 -0 !]
λ  python .\foobar_table.ext --socket \\.\pipe\shell.em --verbose

We see the extension connect to our shell:

I0131 16:07:10.660527  4272 init.cpp:380] osquery initialized [version=3.0.0-17-g10c0c60b]
I0131 16:07:10.663009  4272 rocksdb.cpp:132] Opening RocksDB handle: C:\Users\Nick\work\configs\osquery_extensions\osquery.db
I0131 16:07:10.725503 22132 interface.cpp:338] Extension manager service starting: \\.\pipe\shell.em
I0131 16:07:10.741130 21252 interface.cpp:89] Thrift message: TPipe ::GetOverlappedResult errored GLE=errno = 109
I0131 16:07:10.741130 21252 interface.cpp:89] Thrift message: TConnectedClient died: TPipe: GetOverlappedResult failed
I0131 16:07:10.741130  5972 interface.cpp:89] Thrift message: TPipe ::GetOverlappedResult errored GLE=errno = 109
I0131 16:07:10.741130  5972 interface.cpp:89] Thrift message: TConnectedClient died: TPipe: GetOverlappedResult failed
Using a I0131 16:07:11.811651 25176 events.cpp:746] Starting event publisher run loop: windows_event_log
virtual database. Need help, type '.help'
osquery> I0131 16:07:12.191326  8460 interface.cpp:141] Registering extension (foobar_table, 29953, version=1.0.0, sdk=1.8.0)
I0131 16:07:12.191326  8460 registry.cpp:351] Extension 29953 registered table plugin foobar

We then query the extension:

osquery> select * from foobar;
+-----+-----+
| foo | baz |
+-----+-----+
| bar | baz |
| bar | baz |
+-----+-----+

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.

@muffins muffins added the Windows label Feb 1, 2018
Copy link

@fmanco fmanco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove parenthesis for consistency.

type=TTransportException.UNKNOWN,
message='Failed to write to named pipe: ' + e.message)

def flush(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check if isOpen().

osquery/TPipe.py Outdated

bytesWritten = None
try:
(writeError, bytesWritten) = win32file.WriteFile(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to allow an accept before having created a pipe? Maybe this should just fail if self._handle is None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here with == vs is.


if ret == winerror.ERROR_PIPE_CONNECTED:
win32event.SetEvent(self._overlapped.hEvent)
break
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should raise on else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

self._socket = tempfile.mkstemp(prefix="pyosqsock")
self._pidfile = tempfile.mkstemp(prefix="pyosqpid")
with open(self._pidfile[1], "w") as fh:
fh.write("100000")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you forgot to actually get the PID. Also maybe we should handle errors more gracefully here than allowing IOError to propagate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@muffins muffins force-pushed the osquery-python-windows-port branch 3 times, most recently from 35c6988 to 9e2dceb Compare February 6, 2018 05:26
@muffins muffins force-pushed the osquery-python-windows-port branch from 9e2dceb to 1931549 Compare February 6, 2018 06:10
@muffins
Copy link
Contributor Author

muffins commented Feb 6, 2018

Huh, not entirely sure what I'm doing to screw up tests, however funny thing - tests pass on Windows :D

C:\Users\Nick\work\repos\osquery-python [osquery-python-windows-port]
λ  python setup.py test
running test
running egg_info
writing requirements to osquery.egg-info\requires.txt
writing osquery.egg-info\PKG-INFO
writing top-level names to osquery.egg-info\top_level.txt
writing dependency_links to osquery.egg-info\dependency_links.txt
reading manifest file 'osquery.egg-info\SOURCES.txt'
writing manifest file 'osquery.egg-info\SOURCES.txt'
running build_ext
test_simple_call (tests.test_config_plugin.TestConfigPlugin)
Tests for the call method of osquery.TablePlugin ... ok
test_simple_call (tests.test_logger_plugin.TestLoggerPlugin)
Tests for the call method of osquery.TablePlugin ... ok
test_plugin_inheritance (tests.test_plugin.TestBasePlugin)
Test that an object derived from BasePlugin works properly ... ok
test_singleton_creation (tests.test_singleton.TestSingleton)
Test that two singletons are the same object ... ok
test_plugin_was_registered (tests.test_table_plugin.TestTablePlugin)
Tests to ensure that a plugin was registered ... ok
test_routes_are_correct (tests.test_table_plugin.TestTablePlugin)
Tests to ensure that a plugins routes are correct ... ok
test_simple_call (tests.test_table_plugin.TestTablePlugin)
Tests for the call method of osquery.TablePlugin ... ok

----------------------------------------------------------------------
Ran 7 tests in 0.032s

OK

@muffins
Copy link
Contributor Author

muffins commented Feb 6, 2018

Looks like the only failing test is due to Python 2.6 no longer being supported:

isort requires Python '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' but the running Python is 2.6.9
The command "pip install -r requirements.txt" failed and exited with 1 during .
Your build has been stopped.

Copy link

@friedbutter friedbutter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits mostly.

Generic close method, as both server and client rely on closing pipes
in the same way
"""
if self._handle is not None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not self._handle

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally each param is a new line.

'Failed to open connection to pipe: {}'.format(e))

# Successfully connected, break out.
if h is not None and h.handle != winerror.ERROR_INVALID_HANDLE:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err:

from osquery.extensions.ExtensionManager import Client

DEFAULT_SOCKET_PATH = "/var/osquery/osquery.em"
if sys.platform == "win32":

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"win32" should be a const in thrift spec?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or a const somewhere we can resue?

DEFAULT_SOCKET_PATH = r'\\.\pipe\osquery.em'
else:
DEFAULT_SOCKET_PATH = "/var/osquery/osquery.em"
"""The default path for osqueryd sockets"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment seems out of place

sock = TPipe(pipeName=self.path)
else:
if uuid:
self.path += ".%s" % str(uuid)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be:
self.path += ".{}".format(uuid) if uuid else ""

from thrift.transport import TSocket
from thrift.transport import TTransport

if sys.platform == "win32":

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"win32" is using a lot. plz consider make it a const or part of thrift spec


transport = None
if sys.platform == 'win32':
transport = TPipeServer(args.socket + "." + str(status.uuid))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"{}.{}".format(args.socket, status.uuid)

@muffins muffins force-pushed the osquery-python-windows-port branch from 9065183 to 3ccf6b1 Compare February 21, 2018 19:07
@muffins
Copy link
Contributor Author

muffins commented Feb 25, 2018

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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@ryanheffernan
Copy link

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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch. In fact, awesome to know this works out of the box in py3 :).

@ryanheffernan
Copy link

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.

@ryanheffernan
Copy link

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.

@ryanheffernan
Copy link

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.

@theopolis
Copy link
Member

👍

@theopolis
Copy link
Member

We can remove build for 2.6 in a follow up PR, do not let that stop you from merging this.

@muffins
Copy link
Contributor Author

muffins commented Apr 11, 2018

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 :)

Copy link

@SudhirSingh20 SudhirSingh20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

@muffins
Copy link
Contributor Author

muffins commented Apr 13, 2018

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants