diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/CMakeLists.txt | 19 | ||||
| -rw-r--r-- | python/__init__.py | 11 | ||||
| -rw-r--r-- | python/enterprise.py | 189 | ||||
| -rw-r--r-- | python/generator.cpp | 5 | ||||
| -rw-r--r-- | python/interaction.py | 24 |
5 files changed, 229 insertions, 19 deletions
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 483cbad3..9fd8c6b2 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -6,6 +6,10 @@ file(GLOB PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/*.py) list(REMOVE_ITEM PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/_binaryninjacore.py) list(REMOVE_ITEM PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/enums.py) +if(NOT ENTERPRISE) + list(REMOVE_ITEM PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/enterprise.py) +endif() + add_executable(generator ${PROJECT_SOURCE_DIR}/generator.cpp) target_link_libraries(generator binaryninjaapi) @@ -22,25 +26,12 @@ if(BN_INTERNAL_BUILD) COMMAND ${CMAKE_COMMAND} -E copy ${BN_CORE_OUTPUT_DIR}/binaryninjacore.dll ${PROJECT_BINARY_DIR}/) endif() - option(LICENSE_FILE "Path to license file, for compiling") - set(LICENSE "") - if(LICENSE_FILE) - file(READ ${LICENSE_FILE} LICENSE_CONTS) - message(STATUS "Using licence for build: ${LICENSE_FILE}") - # Encode for passing through the command-line - string(REPLACE "\n" "" LICENSE_CONTS "${LICENSE_CONTS}") - string(REPLACE "\"" "\\\"" LICENSE_CONTS "${LICENSE_CONTS}") - string(REPLACE "{" "\\{" LICENSE_CONTS "${LICENSE_CONTS}") - string(REPLACE "}" "\\}" LICENSE_CONTS "${LICENSE_CONTS}") - set(LICENSE "BN_LICENSE=${LICENSE_CONTS}") - endif() - add_custom_target(generator_copy ALL BYPRODUCTS ${PROJECT_SOURCE_DIR}/_binaryninjacore.py ${PROJECT_SOURCE_DIR}/enums.py DEPENDS ${PYTHON_SOURCES} ${PROJECT_SOURCE_DIR}/../binaryninjacore.h $<TARGET_FILE:generator> COMMAND ${CMAKE_COMMAND} -E echo "Copying Python Sources" COMMAND ${CMAKE_COMMAND} -E make_directory ${BN_RESOURCE_DIR}/python/binaryninja/ - COMMAND ${CMAKE_COMMAND} -E env ASAN_OPTIONS=detect_leaks=0 ${LICENSE} $<TARGET_FILE:generator> ${PROJECT_SOURCE_DIR}/../binaryninjacore.h ${PROJECT_SOURCE_DIR}/_binaryninjacore.py ${PROJECT_SOURCE_DIR}/enums.py + COMMAND ${CMAKE_COMMAND} -E env ASAN_OPTIONS=detect_leaks=0 $<TARGET_FILE:generator> ${PROJECT_SOURCE_DIR}/../binaryninjacore.h ${PROJECT_SOURCE_DIR}/_binaryninjacore.py ${PROJECT_SOURCE_DIR}/enums.py COMMAND ${CMAKE_COMMAND} -E copy ${PYTHON_SOURCES} ${BN_RESOURCE_DIR}/python/binaryninja/ COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/_binaryninjacore.py ${BN_RESOURCE_DIR}/python/binaryninja/ COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/enums.py ${BN_RESOURCE_DIR}/python/binaryninja/ diff --git a/python/__init__.py b/python/__init__.py index 711ed550..1cf00773 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -72,6 +72,9 @@ from .log import (redirect_output_to_log, is_output_redirected_to_log, log_debug, log_info, log_warn, log_error, log_alert, log_to_stdout, log_to_stderr, log_to_file, close_logs) from .log import log as log_at_level +# Only load Enterprise Client support on Enterprise builds +if core.BNGetProduct() == "Binary Ninja Enterprise Client": + from .enterprise import * def shutdown(): @@ -123,6 +126,14 @@ _plugin_init = False def _init_plugins(): global _enable_default_log global _plugin_init + + if not core_ui_enabled() and core.BNGetProduct() == "Binary Ninja Enterprise Client": + # Enterprise client needs to checkout a license reservation or else BNInitPlugins will fail + if not core.BNAuthenticateEnterpriseServerWithMethod("Keychain") and (not core.BNIsLicenseValidated() or not enterprise.is_license_still_activated()): + raise RuntimeError( + "To use Binary Ninja Enterprise from a headless python script, you must check out a license first.\n" + "You can either check out a license for an extended time with the UI, or use the binaryninja.enterprise module.") + if not _plugin_init: # The first call to BNInitCorePlugins returns True for successful initialization and True in this context indicates headless operation. # The result is pulled from BNInitPlugins as that now wraps BNInitCorePlugins. diff --git a/python/enterprise.py b/python/enterprise.py new file mode 100644 index 00000000..8eadb06c --- /dev/null +++ b/python/enterprise.py @@ -0,0 +1,189 @@ +import ctypes +from typing import Tuple, List, Optional + +import binaryninja._binaryninjacore as core +import binaryninja + +if core.BNGetProduct() != "Binary Ninja Enterprise Client": + # None of these functions exist on other builds, so just raise here to notify anyone who tries to use this + raise RuntimeError("Cannot use Binary Ninja Enterprise client functionality with a non-Enterprise client.") + + +def connect(): + """ + Connect to the Enterprise Server. + """ + if not core.BNConnectEnterpriseServer(): + raise RuntimeError(last_error()) + +def is_connected() -> bool: + """ + Determine if the Enterprise Server is currently connected. + :return: True if connected + """ + return core.BNIsEnterpriseServerConnected() + +def authenticate_with_credentials(username: str, password: str): + """ + Authenticate to the Enterprise Server with username/password credentials. + :param str username: Username to use. + :param str password: Password to use. + """ + if not core.BNAuthenticateEnterpriseServerWithCredentials(username, password): + raise RuntimeError(last_error()) + +def authenticate_with_method(method: str): + """ + Authenticate to the Enterprise Server with a non-password method. Note that many of these will + open a URL for a browser-based login prompt, which may not be usable on headless installations. + See :func:`authentication_methods` for a list of accepted methods. + :param str method: Name of method to use. + """ + if not core.BNAuthenticateEnterpriseServerWithMethod(method): + raise RuntimeError(last_error()) + +def authentication_methods() -> List[Tuple[str, str]]: + """ + Get a list of authentication methods accepted by the Enterprise Server. + :return: List of (<method name>, <method display name>) tuples + """ + methods = ctypes.POINTER(ctypes.c_char_p)() + names = ctypes.POINTER(ctypes.c_char_p)() + count = core.BNGetEnterpriseServerAuthenticationMethods(methods, names) + results = [] + for i in range(count): + results.append((core.pyNativeStr(methods[i]), core.pyNativeStr(names[i]))) + core.BNFreeStringList(methods, count) + core.BNFreeStringList(names, count) + return results + +def deauthenticate(): + """ + Deauthenticate from the Enterprise server, clearing any cached credentials. + """ + if not core.BNDeauthenticateEnterpriseServer(): + raise RuntimeError(last_error()) + +def cancel_authentication(): + """ + Cancel a call to :func:`authenticate_with_credentials` or :func:`authenticate_with_method`. + Note those functions are blocking, so this must be called on a separate thread. + """ + core.BNCancelEnterpriseServerAuthentication() + +def is_authenticated() -> bool: + """ + Determine if you have authenticated to the Enterprise Server. + :return: True if you are authenticated + """ + return core.BNIsEnterpriseServerAuthenticated() + +def username() -> Optional[None]: + """ + Get the username of the currently authenticated user to the Enterprise Server. + :return: Username, if authenticated. None, otherwise. + """ + return core.BNGetEnterpriseServerUsername() + +def reservation_time_limit() -> int: + """ + Get the maximum checkout duration allowed by the Enterprise Server. + .. note:: You must authenticate with the Enterprise Server before calling this. + + :return: Duration, in seconds, of the maximum time you are allowed to checkout a license. + """ + return core.BNGetEnterpriseServerReservationTimeLimit() + +def acquire_license(duration, cache=False): + """ + Check out and activate a license from the Enterprise Server. + If ``cache`` is True, the checkout will be saved to a local secrets storage. This is platform-dependent: + - macOS: Saved to the user's keychain + - Windows: Saved to the credential store + - Linux: Saved with dbus's Secret Service API + + .. note:: You must authenticate with the Enterprise Server before calling this. + + .. warning:: If ``cache`` is False, you must remember to call :func:`release_license` before the process + exits to release the uncached license back to the server. If you forget to do so, you will + have to either wait for the checkout to expire or have an administrator revoke the checkout. + + :param int duration: Desired length of license checkout, in seconds. + :param bool cache: If true, the license will be saved to a local secrets storage. + """ + if not core.BNAcquireEnterpriseServerLicense(duration, cache): + raise RuntimeError(last_error()) + +def release_license(): + """ + Release the currently checked out license back to the Enterprise Server. + + .. note:: You must authenticate with the Enterprise Server before calling this. + + .. note:: This will deactivate the Binary Ninja Enterprise client. You must call :func:`acquire_license` + again to continue using Binary Ninja Enterprise in the current process. + """ + if not core.BNReleaseEnterpriseServerLicense(): + raise RuntimeError(last_error()) + +def license_expiration_time() -> int: + """ + Get the expiry time of the current license checkout. + :return: Expiry time as a Unix epoch, or 0 if no license is checked out. + """ + return core.BNGetEnterpriseServerLicenseExpirationTime() + +def license_duration() -> int: + """ + Get the duration of the current license checkout. + :return: Duration, in seconds, of the total time of the current checkout. + """ + return core.BNGetEnterpriseServerLicenseDuration() + +def is_license_still_activated() -> bool: + """ + Determine if your current license checkout is still valid. + :return: True if your current checkout is still valid. + """ + return core.BNIsEnterpriseServerLicenseStillActivated() + +def last_error() -> str: + """ + Get a text representation the last error encountered by the Enterprise Client + :return: Last error message, or empty string if there is none. + """ + return core.BNGetEnterpriseServerLastError() + +class LicenseCheckout: + """ + Helper class for scripts to make use of a license checkout in a scope. + + :Example: + enterprise.connect() + enterprise.authenticate_with_credentials("username", "password") + with enterprise.LicenseCheckout(): + # Do some operation + bv = open_view("/bin/ls") # e.g + # License is released at end of scope + + """ + + def __init__(self, duration=900, cache=False): + self.desired_duration = duration + self.desired_cache = cache + + def __enter__(self): + # UI builds have their own license manager + if binaryninja.core_ui_enabled(): + return + if not is_connected(): + connect() + if not is_authenticated(): + raise RuntimeError("Could not checkout a license: Not authenticated. Please use binaryninja.enterprise.authenticate_with_credentials or authenticate_with_method first!") + acquire_license(self.desired_duration, self.desired_cache) + + def __exit__(self, exc_type, exc_val, exc_tb): + # UI builds have their own license manager + if binaryninja.core_ui_enabled(): + return + release_license() diff --git a/python/generator.cpp b/python/generator.cpp index e0695b66..2e42fa6f 100644 --- a/python/generator.cpp +++ b/python/generator.cpp @@ -160,11 +160,6 @@ int main(int argc, char* argv[]) return 1; } - if (getenv("BN_LICENSE")) - { - BNSetLicense(getenv("BN_LICENSE")); - } - Architecture::Register(new GeneratorArchitecture()); // Parse API header to get type and function information diff --git a/python/interaction.py b/python/interaction.py index 3bbf5fa6..581a87f6 100644 --- a/python/interaction.py +++ b/python/interaction.py @@ -20,6 +20,7 @@ import ctypes import traceback +import webbrowser from typing import Optional # Binary Ninja components @@ -491,6 +492,7 @@ class InteractionHandler: self._cb.getDirectoryNameInput = self._cb.getDirectoryNameInput.__class__(self._get_directory_name_input) self._cb.getFormInput = self._cb.getFormInput.__class__(self._get_form_input) self._cb.showMessageBox = self._cb.showMessageBox.__class__(self._show_message_box) + self._cb.openUrl = self._cb.openUrl.__class__(self._open_url) def register(self): self.__class__._interaction_handler = self @@ -665,6 +667,13 @@ class InteractionHandler: except: log_error(traceback.format_exc()) + def _open_url(self, ctxt, url): + try: + return self.open_url(url) + except: + log_error(traceback.format_exc()) + return False + def show_plain_text_report(self, view, title, contents): pass @@ -715,6 +724,10 @@ class InteractionHandler: def show_message_box(self, title, text, buttons, icon): return MessageBoxButtonResult.CancelButton + def open_url(self, url): + webbrowser.open(url) + return True + class PlainTextReport: def __init__(self, title, contents, view = None): @@ -1302,3 +1315,14 @@ def show_message_box(title, text, buttons=MessageBoxButtonSet.OKButtonSet, icon= :rtype: MessageBoxButtonResult """ return core.BNShowMessageBox(title, text, buttons, icon) + + +def open_url(url): + """ + ``open_url`` Opens a given url in the user's web browser, if available. + + :param str url: Url to open + :return: True if successful + :rtype: bool + """ + return core.BNOpenUrl(url) |
