# Copyright (c) 2015-2024 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import ctypes
import json
from datetime import datetime, date
from typing import List, Dict, Optional
import binaryninja
from . import _binaryninjacore as core
from . import deprecation
from .enums import PluginType
[docs]
class RepoPlugin:
"""
``RepoPlugin`` is mostly read-only, however you can install/uninstall enable/disable plugins. RepoPlugins are
created by parsing the plugins.json in a plugin repository.
"""
def __init__(self, handle: core.BNRepoPluginHandle):
self.handle = handle
def __del__(self):
if core is not None:
core.BNFreePlugin(self.handle)
def __repr__(self):
return f"<{self.path} {'installed' if self.installed else 'not-installed'}/{'enabled' if self.enabled else 'disabled'}>"
@property
def path(self) -> str:
"""Relative path from the base of the repository to the actual plugin"""
result = core.BNPluginGetPath(self.handle)
assert result is not None, "core.BNPluginGetPath returned None"
return result
@property
def subdir(self) -> str:
"""Optional sub-directory the plugin code lives in as a relative path from the plugin root"""
result = core.BNPluginGetSubdir(self.handle)
assert result is not None, "core.BNPluginGetSubdir returned None"
return result
@property
def dependencies(self) -> str:
"""Dependencies required for installing this plugin"""
result = core.BNPluginGetDependencies(self.handle)
assert result is not None, "core.BNPluginGetDependencies returned None"
return result
@property
def installed(self) -> bool:
"""Boolean True if the plugin is installed, False otherwise"""
return core.BNPluginIsInstalled(self.handle)
[docs]
def install(self) -> bool:
"""Attempt to install the given plugin"""
self.install_dependencies()
return core.BNPluginInstall(self.handle)
[docs]
def uninstall(self) -> bool:
"""Attempt to uninstall the given plugin"""
return core.BNPluginUninstall(self.handle)
@installed.setter
def installed(self, state: bool):
if state:
self.install_dependencies()
core.BNPluginInstall(self.handle)
else:
core.BNPluginUninstall(self.handle)
[docs]
def install_dependencies(self) -> bool:
return core.BNPluginInstallDependencies(self.handle)
@property
def enabled(self) -> bool:
"""Boolean True if the plugin is currently enabled, False otherwise"""
return core.BNPluginIsEnabled(self.handle)
@enabled.setter
def enabled(self, state: bool):
if state:
core.BNPluginEnable(self.handle, False)
else:
core.BNPluginDisable(self.handle)
[docs]
def enable(self, force: bool = False) -> bool:
"""
Enable this plugin, optionally trying to force it. \
Force loading a plugin with ignore platform and api constraints. \
(e.g. The plugin author says the plugin will only work on Linux but you'd like to \
attempt to load it on macOS)
"""
return core.BNPluginEnable(self.handle, force)
@property
def api(self) -> List[str]:
"""String indicating the API used by the plugin"""
result: List[str] = []
count = ctypes.c_ulonglong(0)
platforms = core.BNPluginGetApis(self.handle, count)
assert platforms is not None, "core.BNPluginGetApis returned None"
try:
for i in range(count.value):
result.append(platforms[i].decode("utf-8"))
return result
finally:
core.BNFreePluginPlatforms(platforms, count.value)
@property
def description(self) -> Optional[str]:
"""String short description of the plugin"""
return core.BNPluginGetDescription(self.handle)
@property
def license_text(self) -> Optional[str]:
"""String complete license text for the given plugin"""
return core.BNPluginGetLicenseText(self.handle)
@property
def long_description(self) -> Optional[str]:
"""String long description of the plugin"""
return core.BNPluginGetLongdescription(self.handle)
@deprecation.deprecated(deprecated_in="4.0.5366", details='Use :py:func:`minimum_version_info` instead.')
@property
def minimum_version(self) -> int:
"""Minimum version the plugin was tested on"""
return self.minimum_version_info.build
@property
def minimum_version_info(self) -> 'binaryninja.CoreVersionInfo':
"""Minimum version info the plugin was tested on"""
core_version_info = core.BNPluginGetMinimumVersionInfo(self.handle)
return binaryninja.CoreVersionInfo(core_version_info.major, core_version_info.minor, core_version_info.build)
@property
def maximum_version_info(self) -> 'binaryninja.CoreVersionInfo':
"""Maximum version info the plugin will support"""
core_version_info = core.BNPluginGetMaximumVersionInfo(self.handle)
return binaryninja.CoreVersionInfo(core_version_info.major, core_version_info.minor, core_version_info.build)
@property
def name(self) -> str:
"""String name of the plugin"""
result = core.BNPluginGetName(self.handle)
assert result is not None, "core.BNPluginGetName returned None"
return result
@property
def plugin_types(self) -> List[PluginType]:
"""List of PluginType enumeration objects indicating the plugin type(s)"""
result = []
count = ctypes.c_ulonglong(0)
plugintypes = core.BNPluginGetPluginTypes(self.handle, count)
assert plugintypes is not None, "core.BNPluginGetPluginTypes returned None"
try:
for i in range(count.value):
result.append(PluginType(plugintypes[i]))
return result
finally:
core.BNFreePluginTypes(plugintypes)
@property
def project_url(self) -> Optional[str]:
"""String URL of the plugin's git repository"""
return core.BNPluginGetProjectUrl(self.handle)
@property
def package_url(self) -> Optional[str]:
"""String URL of the plugin's zip file"""
return core.BNPluginGetPackageUrl(self.handle)
@property
def author_url(self) -> Optional[str]:
"""String URL of the plugin author's url"""
return core.BNPluginGetAuthorUrl(self.handle)
@property
def author(self) -> Optional[str]:
"""String of the plugin author"""
return core.BNPluginGetAuthor(self.handle)
@property
def version(self) -> Optional[str]:
"""String version of the plugin"""
return core.BNPluginGetVersion(self.handle)
@property
def install_platforms(self) -> List[str]:
"""List of platforms this plugin can execute on"""
result = []
count = ctypes.c_ulonglong(0)
platforms = core.BNPluginGetPlatforms(self.handle, count)
assert platforms is not None, "core.BNPluginGetPlatforms returned None"
try:
for i in range(count.value):
result.append(platforms[i].decode("utf-8"))
return result
finally:
core.BNFreePluginPlatforms(platforms, count.value)
@property
def being_deleted(self) -> bool:
"""Boolean status indicating that the plugin is being deleted"""
return core.BNPluginIsBeingDeleted(self.handle)
@property
def being_updated(self) -> bool:
"""Boolean status indicating that the plugin is being updated"""
return core.BNPluginIsBeingUpdated(self.handle)
@property
def running(self) -> bool:
"""Boolean status indicating that the plugin is currently running"""
return core.BNPluginIsRunning(self.handle)
@property
def update_pending(self) -> bool:
"""Boolean status indicating that the plugin has updates will be installed after the next restart"""
return core.BNPluginIsUpdatePending(self.handle)
@property
def disable_pending(self) -> bool:
"""Boolean status indicating that the plugin will be disabled after the next restart"""
return core.BNPluginIsDisablePending(self.handle)
@property
def delete_pending(self) -> bool:
"""Boolean status indicating that the plugin will be deleted after the next restart"""
return core.BNPluginIsDeletePending(self.handle)
@property
def update_available(self) -> bool:
"""Boolean status indicating that the plugin has updates available"""
return core.BNPluginIsUpdateAvailable(self.handle)
@property
def dependencies_being_installed(self) -> bool:
"""Boolean status indicating that the plugin's dependencies are currently being installed"""
return core.BNPluginAreDependenciesBeingInstalled(self.handle)
@property
def project_data(self) -> Dict:
"""Gets a json object of the project data field"""
data = core.BNPluginGetProjectData(self.handle)
assert data is not None, "core.BNPluginGetProjectData returned None"
return json.loads(data)
@property
def last_update(self) -> date:
"""Returns a datetime object representing the plugins last update"""
return datetime.fromtimestamp(core.BNPluginGetLastUpdate(self.handle))
[docs]
class Repository:
"""
``Repository`` is a read-only class. Use RepositoryManager to Enable/Disable/Install/Uninstall plugins.
"""
def __init__(self, handle: core.BNRepositoryHandle) -> None:
self.handle = handle
def __del__(self) -> None:
if core is not None:
core.BNFreeRepository(self.handle)
def __repr__(self) -> str:
return f"<Repository: {self.path}>"
def __getitem__(self, plugin_path: str):
for plugin in self.plugins:
if plugin_path == plugin.path:
return plugin
raise KeyError()
@property
def url(self) -> str:
"""String URL of the git repository where the plugin repository's are stored"""
result = core.BNRepositoryGetUrl(self.handle)
assert result is not None
return result
@property
def path(self) -> str:
"""String local path to store the given plugin repository"""
result = core.BNRepositoryGetRepoPath(self.handle)
assert result is not None
return result
@property
def full_path(self) -> str:
"""String full path the repository"""
result = core.BNRepositoryGetPluginsPath(self.handle)
assert result is not None
return result
@property
def plugins(self) -> List[RepoPlugin]:
"""List of RepoPlugin objects contained within this repository"""
pluginlist = []
count = ctypes.c_ulonglong(0)
result = core.BNRepositoryGetPlugins(self.handle, count)
assert result is not None, "core.BNRepositoryGetPlugins returned None"
try:
for i in range(count.value):
plugin_ref = core.BNNewPluginReference(result[i])
assert plugin_ref is not None, "core.BNNewPluginReference returned None"
pluginlist.append(RepoPlugin(plugin_ref))
return pluginlist
finally:
core.BNFreeRepositoryPluginList(result)
del result
[docs]
class RepositoryManager:
"""
``RepositoryManager`` Keeps track of all the repositories and keeps the enabled_plugins.json file coherent with
the plugins that are installed/uninstalled enabled/disabled
"""
def __init__(self):
binaryninja._init_plugins()
self.handle = core.BNGetRepositoryManager()
def __getitem__(self, repo_path: str) -> Repository:
for repo in self.repositories:
if repo_path == repo.path:
return repo
raise KeyError()
[docs]
def check_for_updates(self) -> bool:
"""Check for updates for all managed Repository objects"""
return core.BNRepositoryManagerCheckForUpdates(self.handle)
@property
def repositories(self) -> List[Repository]:
"""List of Repository objects being managed"""
result = []
count = ctypes.c_ulonglong(0)
repos = core.BNRepositoryManagerGetRepositories(self.handle, count)
assert repos is not None, "core.BNRepositoryManagerGetRepositories returned None"
try:
for i in range(count.value):
repo_ref = core.BNNewRepositoryReference(repos[i])
assert repo_ref is not None, "core.BNNewRepositoryReference returned None"
result.append(Repository(repo_ref))
return result
finally:
core.BNFreeRepositoryManagerRepositoriesList(repos)
@property
def plugins(self) -> Dict[str, List[RepoPlugin]]:
"""List of all RepoPlugins in each repository"""
plugin_list = {}
for repo in self.repositories:
plugin_list[repo.path] = repo.plugins
return plugin_list
@property
def default_repository(self) -> Repository:
"""Gets the default Repository"""
repo_handle = core.BNRepositoryManagerGetDefaultRepository(self.handle)
assert repo_handle is not None, "core.BNRepositoryManagerGetDefaultRepository returned None"
repo_handle_ref = core.BNNewRepositoryReference(repo_handle)
assert repo_handle_ref is not None, "core.BNNewRepositoryReference returned None"
return Repository(repo_handle_ref)
[docs]
def add_repository(self, url: Optional[str] = None, repopath: Optional[str] = None) -> bool:
"""
``add_repository`` adds a new plugin repository for the manager to track.
To remove a repository, restart Binary Ninja (and don't re-add the repository!).
File artifacts will remain on disk under repositories/ file in the User Folder.
Before you can query plugin metadata from a repository, you need to call ``check_for_updates``.
:param str url: URL to the plugins.json containing the records for this repository
:param str repopath: path to where the repository will be stored on disk locally
:return: Boolean value True if the repository was successfully added, False otherwise.
:rtype: Boolean
:Example:
>>> mgr = RepositoryManager()
>>> mgr.add_repository("https://raw.githubusercontent.com/Vector35/community-plugins/master/plugins.json", "community")
True
>>> mgr.check_for_updates()
>>>
"""
if not isinstance(url, str) or not isinstance(repopath, str):
raise ValueError("Expected url or repopath to be of type str.")
return core.BNRepositoryManagerAddRepository(self.handle, url, repopath)