summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorGlenn Smith <glenn@vector35.com>2021-08-23 19:05:09 -0400
committerGlenn Smith <glenn@vector35.com>2021-11-18 21:37:52 -0500
commit994a42659dc500f602e83f91501ec4a959481fa5 (patch)
treeca783900c9a6d41dbdd98a1cd66a083d3e886578 /python
parent60f2ecd9d1961f7cd700e01f802bcf640c9d50e0 (diff)
Enterprise apis and support
Also secrets provider + openUrl
Diffstat (limited to 'python')
-rw-r--r--python/CMakeLists.txt19
-rw-r--r--python/__init__.py11
-rw-r--r--python/enterprise.py189
-rw-r--r--python/generator.cpp5
-rw-r--r--python/interaction.py24
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)