IdaClu is a version agnostic IDA Pro plugin for grouping similar functions. Pick an existing grouping algorithm or create your own.
#!/usr/bin/python
###############################################################
# Copyright (c) 2023
# Sergejs 'HRLM' Harlamovs <harlamism<at>gmail<dot>com>
# Licensed under the MIT License. All rights reserved.
###############################################################

import collections
import json
import os
import sys

lib_qt = None
try:
    from PyQt5 import QtCore, QtGui, QtWidgets
    lib_qt = "pyqt5"
except ImportError:
    try:
        from PySide import QtCore, QtGui
        from PySide import QtGui as QtWidgets
        lib_qt = "pyside"
    except ImportError:
        pass

is_ida = True
try:  # almost version-agnostic imports
    import idc
    import idaapi
    from idaapi import plugin_t, PluginForm
    from idaclu import ida_shims
except ImportError:
    is_ida = False

    # standalone-run caps
    class plugin_t:
        pass
    class PluginForm:
        pass

from idaclu.qt_shims import (
    QIcon,
    QMessageBox
)
from idaclu import idaclu_gui
from idaclu.assets import resource


# make sub-plugins discoverable
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))


class ScriptEnv():
    def __init__(self, is_ida, lib_qt):
        # generic environment
        self.is_ida = is_ida
        self.lib_qt = lib_qt
        self.run_mode = 'script'
        self.dir_script = SCRIPT_DIR
        self.ver_py = sys.version_info[0]
        # ida-specific environment
        self.detectEnv()

    def __repr__(self):
        return self.get_dump(is_banner=True)

    def dump(self, is_banner=False):
        d_text = self.get_dump(is_banner=is_banner)
        if self.is_ida:
            ida_shims.msg(d_text)
        else:
            print(d_text)

    def get_dump(self, is_banner=False):
        env_repr = []
        if is_banner:
            env_repr.append(self.get_banner())

        env_repr.append('ENVIRONMENT\n')
        for prop in collections.OrderedDict(sorted(self.__dict__.items())):
            value = getattr(self, prop)
            if isinstance(value, list):
                for i, v in enumerate(value):
                    dots = '.' * (80 - (len(prop) + len(str(v))) + 4)
                    prop = " " * len(prop) if i > 0 else prop
                    env_repr.append("{} {} {}".format(prop, dots, v))
            else:
                dots = '.' * (80 - (len(prop) + len(str(value))) + 4)
                env_repr.append("{} {} {}".format(prop, dots, value))
        env_repr.append('\n')
        return "\n".join(env_repr)

    def get_banner(self):
        banner = "                                      \n" \
               + "     ____    __      ________         \n" \
               + "    /  _/___/ /___ _/ ____/ /_  __    \n" \
               + "    / // __  / __ `/ /   / / / / /    \n" \
               + "  _/ // /_/ / /_/ / /___/ / /_/ /     \n" \
               + " /___/\__,_/\__,_/\____/_/\__,_/      \n" \
               + "         by Sergejs Harlamovs         \n" \
               + "                                      \n"
        return banner

    def get_script_mode(self):  # in case of certainty of IDA environment
        mode = 'script'
        if idaapi.IDA_SDK_VERSION >= 720:
            if __name__ == "__main__":
                mode = "script"
            elif __name__.startswith('__plugins__'):
                # __plugins__<plugin_script_name>, ex.: __plugins__idaclu
                mode = "plugin"
        else:
            # maybe even more preferrable way of determining
            # if the plugin was run "as a plugin" or "as a script" -
            # location in one of default plugin directories
            plugin_dirs = ida_shims.get_ida_subdirs('plugins')
            if SCRIPT_DIR in plugin_dirs:
                mode = 'plugin'
            else:
                mode = 'script'
        return mode

    def get_plugin_ort(self):
        plg_loc = None
        plg_scope = 'None'
        g_plg_path = os.path.join(self.dir_plugin[0], 'idaclu')
        l_plg_path = os.path.join(self.dir_plugin[1], 'idaclu')
        if os.path.isdir(g_plg_path):
            plg_loc = self.dir_plugin[0]
            plg_scope = 'global'
        elif os.path.isdir(l_plg_path):
            plg_loc = self.dir_plugin[1]
            plg_scope = 'local'
        return (plg_loc, plg_scope)

    def detectEnv(self):
        if self.is_ida:
            self.ver_sdk = idaapi.IDA_SDK_VERSION
            self.ida_sample = ida_shims.get_input_file_path()
            ##
            self.dir_plugin = ida_shims.get_ida_subdirs("plugins")
            # self.dir_script
            self.feat_bookmarks = self.ver_sdk >= 760
            self.feat_cpp_oop = self.ver_sdk >= 720
            # C++ class hierarchy and virtual function recognition
            self.feat_folders = self.ver_sdk >= 750
            self.feat_golang = self.ver_sdk >= 760
            self.feat_ida6 = self.ver_sdk < 740
            # Point at which IDA v6.95 compatibility APIs was off
            self.feat_ioi64 = self.ver_sdk >= 820
            # "IdaOnIda64" - decompiling of 32-bit files in IDA64
            self.feat_lumina = self.ver_sdk >= 720
            self.feat_microcode = self.ver_sdk >= 710
            self.feat_microcode_new = self.ver_sdk >= 720
            # Microcode feature was introduced in v7.1, improved in v7.2
            self.feat_python3 = self.ver_sdk >= 740
            self.feat_undo = self.ver_sdk >= 730
            self.ida_arch = "x64" if idc.__EA64__ else "x86"
            self.ida_exe = sys.executable
            self.ida_kernel = idaapi.get_kernel_version()
            self.ida_module = os.path.basename(self.ida_sample) if self.ida_sample else None
            self.idb_path = ida_shims.get_idb_path()
            self.is_dbg = idaapi.is_debugger_on()
            # self.is_ida
            self.lib_qt = self.lib_qt = "pyside" if self.ver_sdk < 690 else "pyqt5"
            self.platform = sys.platform
            plg_loc, plg_scope = self.get_plugin_ort()
            self.plg_loc = plg_loc
            self.plg_scope = plg_scope
            self.run_mode = self.get_script_mode()
            self.ver_hexrays = idaapi.get_hexrays_version() if idaapi.init_hexrays_plugin() else None
            # self.ver_py

def common_init():
    env_desc = ScriptEnv(is_ida, lib_qt)
    return env_desc


__AUTHOR__ = "Sergejs 'HRLM' Harlamovs"

PLUGIN_NAME = "IdaClu"
PLUGIN_HOTKEY = 'Ctrl+Alt+O'
PLUGIN_VERSION = '0.9'
PLUGIN_TITLE = '{0} v{1}'.format(PLUGIN_NAME, PLUGIN_VERSION)
PLUGIN_URL = "https://github.com/harlamism/IdaClu"
PLUGIN_INFO = 'For usage see: <a href="{0}">{0}</a>'.format(PLUGIN_URL)

class IdaCluForm(PluginForm):
    def __init__(self, env_desc):
        super(IdaCluForm, self).__init__()
        self.env_desc = env_desc
        self.icon = QIcon(':/idaclu/icon_64.png')
        self.qss = os.path.join(SCRIPT_DIR, 'idaclu', 'assets', 'style.qss')

    def OnCreate(self, form):
        self.env_desc.dump(True)
        if self.env_desc.lib_qt == 'pyqt5':
            self.parent = self.FormToPyQtWidget(form)
        elif self.env_desc.lib_qt == 'pyside':
            self.parent = self.FormToPySideWidget(form)
        self.parent.setWindowTitle(PLUGIN_TITLE)
        self.parent.setWindowIcon(self.icon)
        self.dialog = idaclu_gui.IdaCluDialog(self.env_desc)
        # environment footprint is passed deeper
        self.dialog.setStyleSheet(open(self.qss).read())
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.dialog)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.parent.setLayout(layout)

    def OnClose(self, form):
        pass


def open_form(env_desc):
    if env_desc.is_ida:
        f = IdaCluForm(env_desc)
        f.Show('IdaClu')

def PLUGIN_ENTRY():
    return IdaCluPlugin()

class IdaCluPlugin(plugin_t):
    flags = idaapi.PLUGIN_UNL
    comment = "Function Clusterization Tool"
    help = "Edit->Plugin->IdaClu or {}.".format(PLUGIN_HOTKEY)
    wanted_name = PLUGIN_NAME
    wanted_hotkey = PLUGIN_HOTKEY

    def init(self):
        super(IdaCluPlugin, self).__init__()
        self.icon_id = 0
        ida_shims.msg("%s %s loaded\n" % (self.wanted_name, PLUGIN_VERSION))
        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        env_desc = common_init()
        open_form(env_desc)
        return

    def term(self):
        pass


def main(env_desc):
    env_desc.dump(True)
    if env_desc.is_ida:
        open_form(env_desc)
    else:
        app = QtWidgets.QApplication(sys.argv) if not is_ida else None
        if app:
            sys.exit(app.exec_())

if __name__ == "__main__":
    env_desc = common_init()
    if env_desc.run_mode == 'script':
        if not ida_shims.get_input_file_path():
            QMessageBox.information(None, "File Missing", "Please load a file in IDA first, then run script again.")
        else:
            main(env_desc)

    if not is_ida:
        main(env_desc)

idaclu.py
harlamism/IdaClu
# -*- coding: utf-8 -*-

# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!

from idaclu.qt_shims import (
    QtCore
) 

qt_resource_data = b"\
\x00\x00\x06\xc2\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\
\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\
\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x01\xd8\x00\x00\x01\xd8\
\x01\xfa\x5c\xa6\x72\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\
\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\
\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x06\x3f\x49\x44\
\x41\x54\x78\x9c\xed\x9b\x79\x6c\x14\x55\x18\xc0\x7f\xb3\xb3\x47\
\xb7\xb4\xee\xb6\xf4\x00\x4a\x29\xb7\x50\x08\x8a\x48\x94\xc8\x0d\
\x8d\x28\x60\x90\x10\x8f\x20\x12\x12\x14\x8c\x82\x46\x22\x0a\x48\
\x02\x98\x40\x8c\x8a\x89\x41\x62\x13\x45\x89\x04\x43\xc0\x83\x80\
\x48\x44\x10\x28\xa4\x41\xb0\x18\x40\xae\x02\x16\x28\x81\x1e\x94\
\x6d\xb7\xdb\xee\xd1\x99\xf1\x8f\xdd\x2e\x2d\xec\x6c\xcb\x76\x67\
\x67\xd1\xfd\x25\x9b\x9d\x79\xe7\xf7\x7d\xf3\xde\xf7\xde\x7c\x33\
\x03\x09\x12\x24\x48\xf0\x3f\x46\x68\x23\x7f\x1c\xb0\x0a\xe8\x01\
\x18\x34\xe8\x3f\x35\xf0\xef\xd4\xa0\x6d\x19\xb8\x02\xbc\x0f\x1c\
\x88\xa4\x81\x4f\x01\xe5\x3f\xf2\x5b\xab\xa6\xa4\xda\x08\x18\x07\
\xec\x03\x18\x92\x6c\xa7\xc0\xde\x05\xb3\x10\xdd\x01\xe0\x92\x9b\
\xf8\xec\xfa\x79\x00\x1e\x7c\xa9\x00\x53\x8a\x35\xaa\xed\x4b\x5e\
\x1f\xd7\x0f\x9f\xc2\x71\xee\x6a\x73\xd2\x18\xe0\xe0\x9d\xe5\xd4\
\x0c\x50\x04\x8c\x1c\x92\x6c\x67\xfb\x80\xd1\x18\x85\xb6\x66\xca\
\xbd\x53\xed\xf3\x30\xf4\xc4\x6e\x00\x9e\xda\xb6\x12\x6b\x76\x5a\
\xd4\xfb\x50\x24\x99\xdf\xe7\x7d\xd2\x6c\x84\x83\xf8\x8d\xd0\x0a\
\xb5\xcb\x9a\x0b\x30\xd1\xde\x45\x13\xe5\x63\x85\x20\x1a\xe8\xfa\
\xc4\xe0\xe6\xd3\xbc\x50\x65\xd4\x0c\x20\x02\x58\xa2\x3c\xec\xf5\
\x40\x34\x9b\x82\x87\xa1\xf2\xef\x7f\x0d\x3b\x48\xc2\x00\x7a\x0b\
\xa0\x37\x46\xbd\x05\x00\xb8\xf0\xfd\x01\x9c\x65\x15\x48\x1e\x2f\
\xf6\x01\xb9\x0c\x9c\x3d\x09\x63\x72\x52\x4c\xfa\x8e\x0b\x03\x94\
\x7e\xb7\x2f\x78\x5c\x55\x52\xca\x85\x2d\xfb\x19\xf6\xde\x4c\x7a\
\x4c\x1a\xae\x79\xdf\xba\x4d\x81\x24\x83\x88\x18\x58\x62\xd3\x8c\
\x66\xc6\xd9\xb2\x99\x9c\xd6\x0d\x9b\x68\x42\x91\x64\x8e\xad\xde\
\x44\xcd\xe9\xcb\x9a\xcb\xa1\xdb\x08\x48\x11\x8d\x6c\xea\x37\x82\
\x46\x49\x62\xac\x2d\x0b\x53\x60\xc9\xad\xf6\x79\x98\x71\xfe\x10\
\x17\xdd\xf5\x94\x7c\xb8\x99\x89\x1b\x97\x68\x2a\x87\xae\x4e\x70\
\x64\x6a\x26\x05\xf6\x2e\x41\xe5\x01\x32\x4c\x16\x16\x76\xed\x0f\
\x40\xfd\x95\x4a\xcd\x65\x88\xcb\x55\xa0\xb3\xd1\x02\x80\x22\x2b\
\x9a\xf7\xa5\xab\x01\xd4\xd4\xdb\x5e\x73\x0d\x00\x53\x6a\x74\x6f\
\x90\x42\xa1\x9b\x0f\x90\x14\x85\x27\xcf\xec\xc7\xd1\xe4\x65\x5a\
\x7a\x77\x1e\x4b\xe9\x8c\x55\x14\xf9\xf1\x66\x39\x5b\x6f\x5e\x01\
\xa0\xf7\xb4\x91\x9a\xcb\xa1\x9b\x01\x6a\x25\x1f\xe7\x1a\xeb\x00\
\x28\xac\xb8\x40\x61\xc5\x85\x56\xf9\x69\x03\xf3\xc8\x9f\x3b\x59\
\x73\x39\xe2\x62\x1f\x60\xb1\xa7\xe0\x75\x36\x80\xa2\x60\x4a\xb1\
\xd2\x6b\xda\x48\x06\xbd\x32\x25\x26\x7d\x77\xd8\x00\xe5\xde\x06\
\xd6\xdf\x28\xe5\x50\x5d\x15\x0d\xb2\xc4\x00\xeb\x03\xcc\xc9\xea\
\xcd\x04\x5b\x76\xbb\xdb\x18\xff\xe5\x3b\x9a\xc4\x03\xda\x43\x87\
\x0c\x50\xe2\xba\xc5\xac\xd2\x62\xea\x24\x5f\x30\xad\xc2\xe7\xe6\
\x40\x5d\x25\x6f\x74\xe9\xc7\xbb\x39\xf9\x1d\x16\x50\x6b\x22\x5e\
\x05\xdc\xb2\xc4\xfc\x4b\x47\xa9\x93\x7c\x08\x46\x0b\xe9\x05\xf3\
\xc9\x7a\x7e\x15\xe6\xec\xde\x00\xac\xbb\x51\xca\xde\xda\x8a\xa8\
\x09\xaa\x15\x11\x8f\x80\x5f\x1d\x37\xb8\xee\x6d\x04\xc1\x40\x9f\
\x35\x47\xb0\x74\xed\x07\x40\xe6\xd4\x45\x94\x2e\x1a\x82\xb7\xe2\
\x22\xdf\x54\x5e\xba\xa7\xa9\xa0\x07\x11\x8f\x80\x73\x6e\xbf\x07\
\x37\xa5\x77\x0b\x2a\xdf\x8c\x7d\xcc\xac\x40\x19\x2d\xa2\xdd\xd1\
\x25\x62\x03\x24\x09\xfe\x08\x93\xd2\xe4\xbd\x2b\x4f\x6e\xa8\x0b\
\x94\x89\xcb\x8d\x66\x2b\xc2\x4e\x01\xb7\x2c\x51\xdb\xc2\xc1\xb5\
\x24\xd7\x92\x0c\x40\x53\x5d\x35\xb2\xc7\x85\xc1\xd2\x29\x98\xe7\
\x3a\xb5\x17\x80\x3e\x49\xa9\xaa\xf5\x5b\x3a\x4e\xaf\xab\x11\xa3\
\xd3\x12\x56\x50\x53\x8a\x15\xee\x08\xd0\x4a\x5e\x1f\xb2\x27\x74\
\xfb\xc1\x32\x6d\xe4\xab\x85\x7c\x1d\x80\x2d\x6c\xcd\x16\x64\x4d\
\x5f\x46\xe6\xf4\xa5\xfe\x13\x59\xe6\xef\xd9\x36\x50\xe4\xf6\x56\
\x6f\x17\xb6\xbe\x39\x4c\xd8\xb0\x38\x68\x84\x9b\x27\x2e\x51\xf4\
\xd6\x3a\x64\x5f\x53\x7b\x9b\x70\x03\x77\xed\xad\xa3\x32\x46\xc5\
\xd4\x8c\x16\x67\x0a\x82\xd1\xa4\x5a\x36\x52\xdc\xd5\xb5\xad\xce\
\x7d\xf5\x0d\xf7\xa2\xbc\x2a\x6a\x53\xc0\x09\xd8\x32\xa6\xbe\x4d\
\xda\xf8\xb9\x61\x1b\x10\x93\x6d\x88\x9d\xec\xb7\x13\x0c\x22\xf9\
\x5f\x55\xe2\xbd\x79\x2d\x6c\x3d\xb9\xa1\x96\x8b\xcb\x46\x00\xb0\
\xe7\x6b\x91\x1e\xdd\x42\x97\x1b\xf5\xa2\x44\x65\x0d\x74\x9f\xf0\
\x48\xab\x29\x90\x35\x7c\x20\x16\x7b\x0a\x1e\x47\x3d\xaf\xcf\x14\
\x58\xf8\x72\xe8\x6b\x39\x77\x99\x4c\xd1\x31\x05\xa0\x3a\x54\x7e\
\x58\x1f\x20\x26\xa7\x61\xce\x0c\xf9\x3c\x21\x3c\x06\x63\x9b\xf5\
\x24\x97\x23\x78\x9c\x93\x0d\x3d\x73\x42\x97\xbb\xe5\xf7\xa7\x64\
\x3c\xdc\xb7\x75\x17\x26\x91\xf4\x41\x3d\xb9\x7e\xf8\x14\xe7\xcb\
\xd4\xeb\xb7\x15\x5a\x8c\x7b\x37\x2d\x07\x5c\x89\xd1\x7a\xb7\x93\
\x14\x93\xcc\x00\x34\xba\x23\x8f\x1b\xc4\xbd\x01\xec\x01\x57\x5c\
\xbe\xef\x78\xab\x74\x6f\xad\x8b\xaa\x3f\xfd\x0f\x57\x87\x0d\x8a\
\xfc\xf1\x5d\x5c\xdc\x0d\x86\xe3\xd5\xe7\x04\xd6\x14\x2a\x5c\xfe\
\xe5\x08\x82\x41\x20\xb7\xe0\x51\x3c\x8e\x7a\xce\x6e\xdc\x8d\xc7\
\x51\x8f\x51\x84\xa5\xaf\x45\x7e\x1d\xe3\xde\x00\x2b\x16\x18\xd8\
\x5b\x2c\xf1\xc7\x09\x85\xb2\x9d\xc5\x94\xed\x2c\x0e\xe6\x09\x02\
\xac\x5f\x69\xc0\x9e\x1a\xa6\x81\x36\x88\xfb\x29\x00\x50\xb4\x59\
\x64\xc9\x3c\x81\x74\x3b\x18\x0c\x60\x32\x42\x7e\x5f\x7f\xfa\xec\
\x69\x1d\x7b\x7a\x1d\xb3\x11\xe0\x2c\xd9\x45\xcd\x9e\x2f\x70\x97\
\x9f\xc6\x60\xb6\x62\x09\x44\x7e\xdb\xcb\x8a\x05\x06\x56\x2c\x88\
\xbe\x5c\x31\x31\x40\xc5\x96\xe5\x54\xef\x68\xfd\x96\x8a\xb7\xe2\
\x52\x2c\xba\x6e\x13\xcd\x0d\xe0\x2c\xd9\x15\x54\xbe\x7f\x2f\x98\
\xf3\xac\x80\xc3\x09\x7b\x0e\x2b\xfc\x75\x16\xcc\x26\xc8\x53\xd9\
\x04\xc5\x02\xcd\x0d\x50\xf3\x5b\x21\xe0\x57\xfe\xe4\x8e\xdb\xef\
\x28\xac\x7a\xd3\xff\x2f\xcb\xfe\x79\xad\x17\x9a\x77\xed\xb9\x7a\
\x1a\x40\xd5\x59\xe9\xa9\x3c\xc4\xc0\x00\x42\xe0\x36\xb9\x3c\x4e\
\xa3\x63\x9a\x1b\x20\x65\xf0\x58\x00\x36\x6c\x53\x38\xf7\xcf\xed\
\x74\xa7\x0b\x56\xae\x93\x59\xf7\xad\xf6\x8f\xbf\xc2\xa1\xb9\x0f\
\xc8\x98\xba\x88\xda\xe2\xad\x78\x5c\x0e\x1e\x7a\x46\x22\x27\x1b\
\x3c\x5e\xa8\xbe\x05\x8a\x02\xa0\xf0\xc2\x14\x91\x0c\x7d\xa2\xe2\
\xda\x8f\x00\x53\xe7\x5c\xf2\x16\xff\x84\xa9\x73\x2e\x8a\x02\xe5\
\x37\xa0\xaa\xa6\x59\x79\x3f\xb7\x6a\xd5\xeb\x6b\x4d\x4c\xf6\x01\
\xd6\x3e\xc3\xe9\xfb\xd1\x71\x9c\x25\x3f\xe3\x29\x3f\x83\x60\x4e\
\xc2\x9c\x99\x47\xf9\xe7\x73\x62\xd1\x7d\x58\x62\xb6\x13\x34\x98\
\xad\xd8\x1e\x9f\x11\x3c\x6f\x19\x0f\xd0\x93\xfb\xe2\x5e\x40\x4b\
\x12\x06\xd0\x5b\x00\xbd\x51\x33\x80\x0c\xa0\xf8\xdc\x31\x14\x45\
\x1b\xdc\x9e\xe0\xa1\x14\x2a\x5f\xcd\x00\x97\x01\x9c\xc7\x77\xa1\
\x48\x1d\x0f\x3d\xeb\x85\xdb\x0b\x47\x4f\x06\xd7\xdb\xb2\x50\x65\
\xd4\xa2\x09\x63\x80\xfd\x00\xd6\x5e\x43\x49\x1d\xfa\x34\x82\x29\
\xba\x6f\x6e\xca\xbe\x46\xaa\x7e\x58\x0d\xc0\xa8\x61\x02\xc9\x51\
\x7e\x1d\xc8\xed\xf1\x2b\xdf\x70\x7b\x10\x8f\xc6\xff\x1d\x44\xbb\
\x59\x8b\xfe\x9f\xba\x44\xeb\xf7\xb1\x9a\x92\x6d\xc5\x93\x46\x03\
\x1f\xe0\xff\xd8\x20\xe4\xfb\xf6\x1d\x44\xcb\x8f\xa6\x24\xfc\x53\
\x79\x39\x21\x3e\x95\x49\x90\x20\x41\x82\x04\xc0\xbf\xde\x42\x3d\
\x3c\xe9\x84\xaf\xf0\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\
\x82\
\x00\x00\x02\x1f\
\x00\
\x00\x07\xeb\x78\x9c\xb5\x54\x4d\x6f\x9b\x40\x10\x3d\x9b\x5f\x31\
\x39\xa5\xb6\xc0\x66\x01\x2b\x81\x48\x3d\x24\x6a\x95\x4a\xae\x54\
\x37\x52\x73\x06\xef\x04\xa8\xd7\x2c\x5a\x96\x24\x56\xd4\xff\xde\
\x5d\x42\x1c\x62\xf3\xd5\x43\x2d\x8d\x64\xed\xce\xbc\xf7\xf6\x0d\
\x33\x8b\x19\x7c\xa3\xe1\x0d\x2b\xe1\x4e\xee\x19\x16\x09\xa2\x84\
\x68\x0f\x77\x28\x62\xfc\x5d\xc0\xf9\xed\xcf\xd5\xf7\x73\xb8\x0d\
\x05\x0b\x77\xfc\xb1\x80\xd9\xc2\x58\xcc\xe0\x17\x8a\x22\xe5\x19\
\xd8\x73\x5f\xa5\xe6\x40\x3c\x13\x1c\xdb\x71\xf5\xb5\xb1\xfe\x51\
\x16\xc9\x75\x29\x25\xcf\xe6\x09\x86\x14\x5e\x0c\x50\xbf\x88\x0b\
\x8a\xc2\xda\x70\xc6\x45\x20\xe2\xe8\x13\xb9\x70\xcd\x3a\xa6\x57\
\xc6\xa4\xbe\x2f\xb4\x8e\x00\x0a\xce\x52\xfa\x7e\xfa\x94\x52\x99\
\x04\x40\xf2\x67\x75\xa6\x04\xd4\xc7\x11\x57\x2c\xbb\x00\xec\x2b\
\xcd\x3c\x89\xc2\xcd\x36\x16\xbc\xcc\x68\x4d\x03\x9a\xc7\x71\x6c\
\xb3\x8e\xe9\xc4\xf8\x63\xac\xef\x53\x1a\xa3\x9c\x3f\x88\x70\x87\
\xf0\x19\xd6\x5f\xab\x3f\x2f\x6f\x64\x81\x62\x79\xe5\xaf\xca\x3d\
\x62\xc2\x6b\x28\x95\x9d\xd5\x2d\x30\xb6\x16\xdb\x57\xb0\x0a\x23\
\x64\x2d\x05\x0d\xff\x0a\x64\xb8\x91\xd6\x7f\xb1\xb1\x3e\x13\x69\
\x9c\x48\x6d\xe1\xe1\xa4\xe1\x1d\x71\x6d\x93\xb8\x4b\x93\x78\x5e\
\x05\x3e\x68\xf0\x91\xfe\x9c\x49\x2b\x92\xd9\xfb\x23\xc7\x0a\xe7\
\xa5\x2c\x50\xb6\x2b\x6f\xa5\x08\x36\x09\x6e\xb6\x48\x5b\xa9\x6c\
\x13\x2e\xd5\x27\x4a\x96\xee\xb4\xa3\xdc\xda\x23\x63\xfc\xa9\xaa\
\x6e\x7f\xe4\x72\x69\x56\xe1\x38\x03\x18\x1f\x94\xf4\x62\x11\x9f\
\x74\x62\x45\xac\xc4\x1e\x04\xd5\x97\x1a\xa5\x17\x61\x58\x0b\xf1\
\xfd\x41\xa4\x58\x20\x66\x3d\x62\x1c\x67\xd0\x9a\x0a\x62\x8c\x1a\
\x32\xe8\x4c\x9e\x66\xdb\x01\x6f\xb5\x20\xef\xa2\x17\x61\x5c\x97\
\x2a\x3d\xae\x7f\x82\x24\x39\x67\xf5\x87\x3d\x3c\x94\x30\x7e\x2a\
\xa1\xdb\x63\xfd\xac\x2a\x3a\xc5\x58\x09\x7f\x0c\x54\xa0\xe8\x92\
\xa5\x86\x59\xcf\x29\x59\x4e\xfb\x98\xd4\x07\xe1\xe9\x36\x9c\xb6\
\xe0\x8d\xa9\x61\x5e\x07\x8f\x1a\xb7\x6a\xda\xba\x69\x6c\x4f\x3d\
\xe6\xb2\xb5\x4f\x48\xd3\xc3\xce\x3b\x59\x53\xa0\x93\x57\x69\x86\
\x5f\x54\x56\x2d\x20\x0f\x29\x4d\xb3\xd8\x62\xf8\xa0\x72\x3c\xbd\
\x24\x46\xec\xca\x46\xca\xc7\xc6\x34\x2e\x8e\xf6\xce\x0d\xdf\x45\
\xfc\x9a\x3f\x07\x98\x85\x11\x3b\x18\xd0\xc2\xdf\x4c\x3e\xeb\xcb\
\xb6\x8f\xb2\xc7\xed\xf9\x7f\xd7\xfe\x17\x40\x88\x7b\xb8\
\x00\x00\x01\x4e\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\
\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0e\xc4\x00\x00\x0e\xc4\
\x01\x95\x2b\x0e\x1b\x00\x00\x01\x00\x49\x44\x41\x54\x58\x85\xed\
\x94\x41\x0a\xc2\x30\x10\x45\x1f\x9e\x40\x8a\x14\x11\x8f\x23\x2e\
\x8a\x0b\x6f\x26\x22\x22\x22\x45\xc4\xa3\x88\x4b\x11\x11\x29\xe2\
\x41\x8a\xab\xba\x49\x60\x94\x9a\x34\xb1\xb5\x9b\x7c\x98\x4d\x33\
\xf9\xef\x51\x68\x21\x24\x24\xe4\x3d\x53\xe0\x08\x44\x0d\x74\x77\
\x81\x03\x30\x31\xc1\x9f\x40\x01\x9c\x81\x5e\x8d\xf0\x08\x38\xa9\
\xee\x1c\x48\x3e\x17\x12\x01\xd7\x73\xa9\x49\xa2\xa7\xba\x64\x77\
\x0e\x8c\xf5\xc2\x58\x3d\x28\x4a\xe6\x0a\xc4\x3f\xc0\xe3\x12\xb8\
\x94\x18\x01\xac\xbf\x2c\x48\x89\xbe\x27\xfc\x6a\xe9\xde\x00\x74\
\x80\x9d\x65\xf1\xe6\x28\xd1\x57\x77\x4c\x9d\x7b\xc5\xa6\xaa\x44\
\x56\x51\xa2\x0a\x7c\x27\xe1\xae\x12\x03\x0b\x3c\xf3\x81\x4b\x89\
\xbd\xa5\xe0\xfe\x45\x62\xa0\xce\x4c\x77\xb7\x26\xb8\x94\xb0\xbd\
\x89\x3b\x30\x74\x84\xa7\x55\xe0\x3e\x12\xc3\xba\xe1\x52\x62\x6b\
\x29\x7e\xa8\xb1\x7d\x6a\xce\x70\x17\x89\xc6\xe0\x52\x22\xf5\x80\
\xaf\xeb\x80\x4b\x89\x4d\x5b\x70\x29\x61\xfb\x6d\x17\xc0\xaa\x09\
\xb8\x94\x58\xb5\x05\x97\x59\xb4\x09\xd7\x99\x0b\xf8\xf2\xdf\x70\
\x9d\x99\x82\x87\x84\x84\x78\xe7\x05\xa5\x72\xea\xe1\x7e\x85\x89\
\x6e\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
"

qt_resource_name = b"\
\x00\x06\
\x06\xfa\x7a\x35\
\x00\x69\
\x00\x64\x00\x61\x00\x63\x00\x6c\x00\x75\
\x00\x0b\
\x02\x48\x1b\xc7\
\x00\x69\
\x00\x63\x00\x6f\x00\x6e\x00\x5f\x00\x36\x00\x34\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x09\
\x00\x28\xad\x23\
\x00\x73\
\x00\x74\x00\x79\x00\x6c\x00\x65\x00\x2e\x00\x71\x00\x73\x00\x73\
\x00\x0c\
\x0f\x76\x7a\xe7\
\x00\x61\
\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x33\x00\x32\x00\x2e\x00\x70\x00\x6e\x00\x67\
"

qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\
\x00\x00\x00\x2e\x00\x01\x00\x00\x00\x01\x00\x00\x06\xc6\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x00\x08\xe9\
"

qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x2e\x00\x01\x00\x00\x00\x01\x00\x00\x06\xc6\
\x00\x00\x01\x8a\xcb\xbe\x2b\x59\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x8a\x24\xd1\x7e\x8e\
\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x00\x08\xe9\
\x00\x00\x01\x8a\x7b\xda\x01\xd7\
"

qt_version = [int(v) for v in QtCore.qVersion().split('.')]
if qt_version < [5, 8, 0]:
    rcc_version = 1
    qt_resource_struct = qt_resource_struct_v1
else:
    rcc_version = 2
    qt_resource_struct = qt_resource_struct_v2

def qInitResources():
    QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)

def qCleanupResources():
    QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)

qInitResources()

idaclu/assets/resource.py
harlamism/IdaClu
# Shim file to support IDA 6.x-8.x
# Documentation provided by Hex-Rays:
# https://hex-rays.com/products/ida/support/ida74_idapython_no_bc695_porting_guide/
# https://hex-rays.com/products/ida/support/ida74_idapython_no_bc695_porting_guide.shtml

import os
import sys
import idc
import idaapi
import idautils


try:
    import ida_bytes
except ImportError:
    ida_bytes = None

try:
    import ida_name
except ImportError:
    ida_name = None

try:
    import ida_kernwin
except ImportError:
    ida_kernwin = None

try:
    import ida_nalt
except ImportError:
    ida_nalt = None

try:
    import ida_ua
except ImportError:
    ida_ua = None

try:
    import ida_funcs
except ImportError:
    ida_funcs = None


def _get_fn_by_version(lib, curr_fn, archive_fn, archive_lib=None):
    '''
    Determine which function should be called based on the version of IDA.

    :param curr_fn: 7.X version of the function.

    :param archive_fn: 6.X version of the function.

    :param archive_lib: If the archive lib is different than the current lib,
                        set it here.

    :return: Function based on the version of IDA.
    '''
    if idaapi.IDA_SDK_VERSION >= 700:
        try:
            return getattr(lib, curr_fn)
        except AttributeError:
            raise Exception('%s is not a valid function in %s' % (curr_fn, lib))
    use_lib = lib if archive_lib is None else archive_lib
    try:
        return getattr(use_lib, archive_fn)
    except AttributeError:
        raise Exception('%s is not a valid function in %s' % (archive_fn, use_lib))


def print_insn_mnem(ea):
    '''
    Get instruction mnemonics.

    :param ea: Linear address of the instruction.
    :type ea: int

    :return: Instruction mnemonic. "" if not instruction is found.

    :note: *Heavy breath* This function may not return exactly the same
    mnemonics as you see on the screen.
    '''
    fn = _get_fn_by_version(idc, 'print_insn_mnem', 'GetMnem')
    return fn(ea)


def print_operand(ea, n):
    '''
    Get operand of an instruction or data.

    :param ea: Linear address of the item.
    :type ea: int

    :param n: Number of operand: 0 - the first operand 1 - the second operand.
    :type n: int

    :return: The current text representation of operand or "".
    '''
    fn = _get_fn_by_version(idc, 'print_operand', 'GetOpnd')
    return fn(ea, n)


def get_switch_info(ea):
    fn = _get_fn_by_version(idaapi, 'get_switch_info', 'get_switch_info_ex', idaapi)
    return fn(ea)

def get_operand_type(head, opnd_index):
    '''
    '''
    fn = _get_fn_by_version(idc, 'get_operand_type', 'GetOpType')
    return fn(head, opnd_index)

def define_local_var(start, end, location, name):
    '''
    Create a local variable.

    :param start: Start address range for the local variable.
    :type start: int

    :param end: End of address range for the local variable.
    :type end: int

    :param location: The variable location in the "[bp+xx]" form where xx is
                     a number. The location can also be specified as a
                     register name.
    :type location: str

    :param name: Name of the local variable.
    :type name: str

    :return: 1-ok, 0-failure
    '''
    fn = _get_fn_by_version(idc, 'define_local_var', 'MakeLocal')
    return fn(start, end, location, name)


def find_func_end(ea):
    '''
    Determine a new function boundaries.

    :param ea: Start address of the new function.
    :type ea: int

    :return: If a function already exists, then return its end address. If a
    function end cannot be determine, the return BADADDR otherwise return the
    end address of the new function.
    '''
    fn = _get_fn_by_version(idc, 'find_func_end', 'FindFuncEnd')
    return fn(ea)


def is_code(flag):
    '''
    Does flag denote start of an instruction.
    :param flag: Flag for an instruction.
    :type flag: int

    :return: True if flags indicate code, False otherwise.
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_code', 'isCode', idaapi)
    return fn(flag)


def is_data(flag):
    '''
    Does flag denote start of an instruction.
    :param flag: Flag for an instruction.
    :type flag: int

    :return: True if flags indicate code, False otherwise.
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_code', 'isData', idaapi)
    return fn(flag)


def is_dword(flag):
    '''
    Does flag denote start of an instruction.
    :param flag: Flag for an instruction.
    :type flag: int

    :return: True if flags indicate code, False otherwise.
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_dword', 'isDwrd', idaapi)
    return fn(flag)


def is_qword(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_qword', 'isQwrd', idaapi)
    return fn(flag)

def is_word(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_word', 'isWord', idaapi)
    return fn(flag)


def is_struct(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_struct', 'isStruct', idaapi)
    return fn(flag)

def is_float(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_float', 'isFloat', idaapi)
    return fn(flag)


def is_double(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_double', 'isDouble', idaapi)
    return fn(flag)


def is_enum0(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_enum0', 'isEnum0', idaapi)
    return fn(flag)


def is_off0(flag):
    '''
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_off0', 'isOff0', idaapi)
    return fn(flag)

def is_loaded(ea):
    fn = _get_fn_by_version(ida_bytes, 'is_loaded', 'isLoaded', idc)
    return fn(ea)

def get_flags(ea):
    '''
    Get flags value for address 'ea'

    :param ea: Linear address.
    :type ea: int

    :return: 0 if flags not present in the program
    '''
    fn = _get_fn_by_version(ida_bytes, 'get_flags', 'get_flags_novalue')
    return fn(ea)


def get_full_flags(ea):
    '''
    Get flags value for address 'ea'

    :param ea: Linear address.
    :type ea: int

    :return: 0 if flags not present in the program
    '''
    fn = _get_fn_by_version(ida_bytes, 'get_full_flags', 'getFlags', idaapi)
    return fn(ea)


def get_idb_path():
    '''
    '''
    fn = _get_fn_by_version(idc, 'get_idb_path', 'GetIdbPath', idc)
    return fn()


def ua_mnem(ea):
    '''
    '''
    fn = _get_fn_by_version(ida_ua, 'ua_mnem', 'GetMnem', idc)
    return fn(ea)


def get_operand_value(ea, idx):
    '''
    '''
    fn = _get_fn_by_version(idc, 'get_operand_value', 'GetOperandValue', idc)
    return fn(ea, idx)


def get_bytes(func_addr, func_size):
    '''
    '''
    fn = _get_fn_by_version(idc, 'get_bytes', 'GetManyBytes', idc)
    return fn(func_addr, func_size)


def get_name(ea):
    '''
    Get name at the specified address.

    :param ea: Linear address
    :type ea: int

    :return: "" - byte has no name.
    '''
    fn = _get_fn_by_version(idc, 'get_name', 'Name')

    if idaapi.IDA_SDK_VERSION > 700:
        return fn(ea, ida_name.GN_VISIBLE)
    return fn(ea)


def generate_disasm_line(ea, flags):
    fn = _get_fn_by_version(idc, 'generate_disasm_line', 'GetDisasmEx')
    return fn(ea, flags)


def get_func_off_str(ea):
    '''
    Convert address to 'funcname+offset' string.

    :param ea: Address to convert.
    :type ea: int

    :return: If the address belongs to a function then return a string formed as
    'name+offset' where 'name' is a function name, 'offset' is offset within
    the function else return null string.
    '''
    fn = _get_fn_by_version(idc, 'get_func_off_str', 'GetFuncOffset')
    return fn(ea)


def jumpto(ea, opnum=-1, uijmp_flags=0x0001):
    '''
    Jump to the specified address.

    :param ea: Destination
    :type ea: int

    :param opnum: -1: don't change the x coord.
    :type opnum: int

    :param uijmp_flags: Jump flags.
    :type uijmp_flags: int

    :return: success
    '''
    fn = _get_fn_by_version(ida_kernwin, 'jumpto', 'Jump', idc)
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, opnum, uijmp_flags)
    return fn(ea)


def ask_yn(default, format_str):
    '''
    Display a dialog box and get choice from "Yes", "No", "Cancel".

    :param default: Default choice: one of Button IDs
    :type default: int

    :param format_str: The question in printf() style format.
    :type format_str: str

    :return: The selected button (one of Button IDs).
    '''
    fn = _get_fn_by_version(ida_kernwin, 'ask_yn', 'AskYN', idc)
    return fn(default, format_str)


def ask_file(for_saving, default, dialog):
    '''
    Get file from user.

    :param for_saving: File is for saving.
    :type for_saving: int

    :param default: File extension.
    :type default: str

    :param dialog: Dialog box to display to the user.
    :type dialog: str

    :return: file path.
    '''
    fn = _get_fn_by_version(ida_kernwin, 'ask_file', 'AskFile', idc)
    return fn(for_saving, default, dialog)


def get_func_attr(ea, attr):
    '''
    Get a function attribute.

    :param ea: Any address belonging to the function.
    :type ea: int

    :param attr: One of FUNCATTR_... constants

    :return: BADADDR - error otherwise returns the attribute value.
    '''
    fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionAttr')
    return fn(ea, attr)


def get_name_ea_simple(name):
    '''
    Get linear address of a name.

    :param name: Name of program byte.
    :type name: str

    :return: Address of the name or BADADDR - No such name.
    '''
    fn = _get_fn_by_version(idc, 'get_name_ea_simple', 'LocByName')
    return fn(name)


def get_name_ea(afrom, fname):
    '''
    '''
    fn = _get_fn_by_version(idaapi, 'get_name_ea', 'LocByNameEx', idc)
    return fn(afrom, fname)


def next_head(ea, maxea=4294967295):
    '''
    Get next defined item (instruction or data) in the program.

    :param ea: Linear address to start search from.
    :type ea: int

    :param maxea: The search will stop at the address maxea. maxea is not
                  included in the search range
    :type maxea: int

    :return: BADADDR - no (more) defined items
    '''
    fn = _get_fn_by_version(idc, 'next_head', 'NextHead')
    return fn(ea, maxea)


def get_screen_ea():
    '''
    Return the linear address of the current screen location.

    :return: Address of screen focus.
    '''
    fn = _get_fn_by_version(idc, 'get_screen_ea', 'ScreenEA')
    return fn()


def choose_func(title):
    '''
    Ask the user to select a function.

    :param title: Title of the dialog box.
    :type title: str

    :return: -1 user refused to select a function, otherwise function start addr
    '''
    fn = _get_fn_by_version(idc, 'choose_func', 'ChooseFunction')
    return fn(title)


def ask_ident(default, prompt):
    '''
    Ask for a long text.
    :param default: The default value.
    :type default: str

    :param prompt: The prompt value.
    :type prompt: str

    :return: None or the entered string.
    '''
    fn = _get_fn_by_version(ida_kernwin, 'ask_str', 'AskIdent', idc)
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(default, ida_kernwin.HIST_IDENT, prompt)
    return fn(default, prompt)


def set_name(ea, name, flags):
    '''
    Rename an address.

    :param ea: Linear address.
    :type ea: int

    :param name: New name of address. If name == "" then delete old name.
    :type name: str

    :return: 1-ok, 0-failure
    '''
    fn = _get_fn_by_version(idc, 'set_name', 'MakeName')
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, name, flags)
    return fn(ea, name)


def get_wide_dword(ea):
    '''
    Get one wide word of the program at 'ea'
    :param ea: linear address.
    :type ea: int

    :return: uint64
    '''
    fn = _get_fn_by_version(idc, 'get_wide_dword', 'Dword')
    return fn(ea)


def get_strlit_contents(str_obj):
    '''
    Get string contents.

    :param ea: Linear address.
    :type ea: int

    :return: String contents or empty string.
    '''
    fn = _get_fn_by_version(ida_bytes, 'get_strlit_contents', 'GetString', idc)
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(str_obj.ea, str_obj.length, str_obj.strtype)
    return fn(str_obj.ea)


def get_func_name(ea):
    '''
    Retrieve function name.

    :param ea: Any address belonging to the function.
    :type ea: int

    :return: Null string if not found, otherwise the functions name.
    '''
    fn = _get_fn_by_version(idc, 'get_func_name', 'GetFunctionName')
    return fn(ea)


def get_first_seg():
    '''
    Get first segment.

    :return: Address of the start of the first segment or BADADDR if no
    segments found.
    '''
    fn = _get_fn_by_version(idc, 'get_first_seg', 'FirstSeg')
    return fn()


def get_segm_attr(segea, attr):
    '''
    Get segment attribute.

    :param segea: Any address within the segment.
    :type segea: int

    :param attr: One of SEGATTR_... constants.
    :type attr: int

    :return: Segment attributes.
    '''
    fn = _get_fn_by_version(idc, 'get_segm_attr', 'GetSegmentAttr')
    return fn(segea, attr)


def get_next_seg(ea):
    '''
    Get next segment.

    :param ea: Linear address.
    :type ea: int

    :return: Start of the next segment or BADADDR
    '''
    fn = _get_fn_by_version(idc, 'get_next_seg', 'NextSeg')
    return fn(ea)


def is_strlit(flags):
    '''
    Do flags indicate a string.

    :param flags: Flags for address.
    :type flags: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_strlit', 'isASCII', idc)
    return fn(flags)


def create_strlit(start, length):
    '''
    Convert to string literal and give a meaningful name.

    :param start: Start ea.
    :type start: int

    :param length: Length of string, or 0 to determine dynamically.
    :type length: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'create_strlit', 'MakeStr', idc)
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(start, length, ida_nalt.STRTYPE_C)
    return fn(start, idc.BADADDR)


def is_unknown(flags):
    '''
    Do flags indicate an unknown type.

    :param flags: Flags for address.
    :type flags: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_unknown', 'isUnknown', idc)
    return fn(flags)


def refresh_idaview_anyway():
    '''
    '''
    fn = _get_fn_by_version(idaapi, 'refresh_idaview_anyway', 'Refresh', idc)
    return fn()

def get_current_widget():
    '''
    '''
    fn = _get_fn_by_version(idaapi, 'get_current_widget', 'get_current_tform', idaapi)
    return fn()


def get_widget_vdui(form_widget_ref):
    '''
    '''
    fn = _get_fn_by_version(idaapi, 'get_widget_vdui', 'get_tform_vdui', idaapi)
    return fn(form_widget_ref)


def is_byte(flags):
    '''
    Do flags indicate a byte type.

    :param flags: Flags for address.
    :type flags: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_byte', 'isByte', idc)
    return fn(flags)

def is_char0(flags):
    '''
    Do flags indicate a byte type.

    :param flags: Flags for address.
    :type flags: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'is_char0', 'isChar0', idc)
    return fn(flags)


def create_dword(ea):
    '''
    Convert to data.

    :param ea: Linear address .
    :type ea: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_bytes, 'create_data', 'MakeDword', idc)
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, ida_bytes.FF_DWORD, 4, idaapi.BADADDR)
    return fn(ea)


def op_plain_offset(ea, n, base):
    '''
    Convert operand to an offset.

    :param ea: Linear address.
    :type ea: int

    :param n: Number of operands.
    :type n: int

    :param base: Base of the offset.
    :type base: int

    :return:
    '''
    fn = _get_fn_by_version(idc, 'op_plain_offset', 'OpOff')
    return fn(ea, n, base)


def next_addr(ea):
    '''
    Get next address in the program.

    :param ea: Linear address.
    :type ea: int

    :return: Next address or BADADDR
    '''
    fn = _get_fn_by_version(ida_bytes, 'next_addr', 'NextAddr', idc)
    return fn(ea)


def can_decode(ea):
    '''
    Can the bytes at ea be decoded as an instruction?

    :param ea: Linear address
    :type ea: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_ua, 'can_decode', 'decode_insn', idaapi)
    return fn(ea)


def get_operands(insn):
    '''
    Get operands for the current address.

    :return:
    '''
    if idaapi.IDA_SDK_VERSION >= 700:
        return insn.ops
    return idaapi.cmd.Operands


def get_canon_feature(insn):
    '''
    Get operands for the provided instruction.

    :return:
    '''
    if idaapi.IDA_SDK_VERSION >= 700:
        return insn.get_canon_feature()
    return idaapi.cmd.get_canon_feature()


def get_segm_name(ea):
    '''
    Get name of a segment.

    :param ea: Any address within the segment.
    :type ea: int

    :return: Segement name.
    '''
    fn = _get_fn_by_version(idc, 'get_segm_name', 'SegName')
    return fn(ea)


def add_func(ea):
    '''
    Add a new function.

    :param ea: Start address.
    :type ea: int

    :return: bool
    '''
    fn = _get_fn_by_version(ida_funcs, 'add_func', 'MakeFunction', idc)
    return fn(ea)


def create_insn(ea):
    '''
    Create instruction.

    :param ea: Linear address
    :type ea: int

    :return: bool
    '''
    fn = _get_fn_by_version(idc, 'create_insn', 'MakeCode')
    return fn(ea)


def get_segm_end(ea):
    '''
    Get end address of a segment.

    :param ea: Linear address
    :type ea: int

    :return: Address
    '''
    fn = _get_fn_by_version(idc, 'get_segm_end', 'SegEnd')
    return fn(ea)


def get_segm_start(ea):
    '''
    Get start address of a segment.

    :param ea: Linear address
    :type ea: int

    :return: Address
    '''
    fn = _get_fn_by_version(idc, 'get_segm_start', 'SegStart')
    return fn(ea)


def insn_t():
    prop = _get_fn_by_version(idaapi, 'insn_t', 'cmd', idaapi)
    if idaapi.IDA_SDK_VERSION >= 700:
        return prop()
    else:
        return prop


def decode_insn(ea):
    """
    Decode instruction.
    :param ea: Linear address.
    :type ea: int

    :return: Instruction at ea.
    """
    fn = _get_fn_by_version(ida_ua, 'decode_insn', 'decode_insn', idaapi)
    if idaapi.IDA_SDK_VERSION >= 700:
        insn = ida_ua.insn_t()
        fn(insn, ea)
        return insn
    fn(ea)
    return idaapi.cmd


def get_bookmark(index):
    """
    Get bookmark

    :param index: Index of bookmark
    :type index: int

    :return: Address of bookmark
    """
    fn = _get_fn_by_version(idc, 'get_bookmark', 'GetMarkedPos')
    return fn(index)


def get_bookmark_desc(index):
    """
    Get bookmark description.

    :param index: Index of bookmark
    :type index: int

    :return:
    """
    fn = _get_fn_by_version(idc, 'get_bookmark_desc', 'GetMarkComment')
    return fn(index)


def set_color(ea, what, color):
    """
    Set item color.

    :param ea: Linear address.
    :type ea: int

    :param what: Type of the item, one of CIC_... contstants
    :type what: int

    :param color: New color code in RGB.
    :type color: int

    :return: bool
    """
    fn = _get_fn_by_version(idc, 'set_color', 'SetColor')
    return fn(ea, what, color)


def get_color(ea, what):
    """
    """
    fn = _get_fn_by_version(idc, 'get_color', 'GetColor')
    return fn(ea, what)


def msg(message):
    """
    Display a UTF-8 string in the message window.

    :param message: Message to print.
    :type message: str

    :return: PyObject * (what?)
    """
    fn = _get_fn_by_version(ida_kernwin, 'msg', 'Message', idc)
    return fn(message)


def get_highlighted_identifier():
    """
    Get currently highlighted text.

    :return: Highlighted text or ""
    """
    fn = _get_fn_by_version(ida_kernwin, 'get_highlight',
                            'get_highlighted_identifier', idaapi)

    if idaapi.IDA_SDK_VERSION >= 700:
        viewer = ida_kernwin.get_current_viewer()
        highlight = fn(viewer)
        if highlight and highlight[1]:
            return highlight[0]
    return fn()


def start_ea(obj):
    """
    Return start ea for supplied object.

    :param obj: Object to retrieve start ea.

    :return: start ea.
    """
    if not obj:
        return None

    try:
        return obj.startEA
    except AttributeError:
        return obj.start_ea


def end_ea(obj):
    """
    Return end ea for supplied object.

    :param obj: Object to retrieve end ea.

    :return: end ea.
    """
    if not obj:
        return None

    try:
        return obj.endEA
    except AttributeError:
        return obj.end_ea


def set_func_flags(ea, flags):
    """
    Change function flags.

    :param ea: Any address belonging to the function.
    :type ea: int

    :param flags: Flags to set.
    :type flags: int

    :return: 0 - ok
    """
    fn = _get_fn_by_version(idc, 'set_func_attr', 'SetFunctionFlags')
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, idc.FUNCATTR_FLAGS, flags)
    return fn(ea, flags)


def set_func_attr(ea, flags, value):
    """
    """
    fn = _get_fn_by_version(idc, 'set_func_attr', 'SetFunctionFlags')
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, 36, value)
    return fn(ea, 36, value)


def get_func_flags(ea):
    """
    Get function flags.

    :param ea: Any address belonging to the function.
    :type ea: int

    :return: Flags
    """
    fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionFlags')
    if idaapi.IDA_SDK_VERSION >= 700:
        return fn(ea, idc.FUNCATTR_FLAGS)
    return fn(ea)


def get_input_file_path():
    fn = _get_fn_by_version(idaapi, 'get_input_file_path', 'GetInputFilePath', idc)
    return fn()


def get_ida_subdirs(sub_folder):
    if idaapi.IDA_SDK_VERSION > 700:
        return idaapi.get_ida_subdirs("plugins")
    else:
        USR_PLUGIN_PATH = os.path.join(idaapi.get_user_idadir(), sub_folder)
        SYS_PLUGIN_PATH = os.path.join(idaapi.idadir(idaapi.PLG_SUBDIR))
        return [USR_PLUGIN_PATH, SYS_PLUGIN_PATH]


def _is_func_chunked(func_addr):
    """
    Check if a function is divided into chunks.
    """
    # logger.debug('is_func_chunked {}'.format(func_addr))
    # Idea for this code is from:
    # http://code.google.com/p/idapython/source/browse/trunk/python/idautils.py?r=344

    num_chunks = 0
    func_iter = idaapi.func_tail_iterator_t(idaapi.get_func(func_addr))
    status = func_iter.main()
    while status:
        chunk = func_iter.chunk()
        num_chunks += 1
        # yield (chunk.startEA, chunk.endEA)
        status = func_iter.next()

    return (num_chunks > 1)


def calc_func_size(func_desc):
    beg_addr = start_ea(func_desc)
    if idaapi.IDA_SDK_VERSION >= 720:
        return ida_funcs.calc_func_size(func_desc)
    else:
        if _is_func_chunked(beg_addr):
            func_size = 0
            for beg, end in idautils.Chunks(beg_addr):
                func_size = end - beg
            return func_size
        else:
            end_addr = end_ea(func_desc)
            if end_addr > beg_addr:
                return end_addr - beg_addr
            else:
                return 0

idaclu/ida_shims.py
harlamism/IdaClu
import idc
import idaapi
import idautils

# new backward-incompatible modules
try:
    import ida_dirtree
    from ssdeep import (
        hash as ssdeep_hash,
        compare as ssdeep_compare
    )
    from tlsh import (
        hash as tlsh_hash,
        diff as tlsh_diff
    )
except ImportError:
    pass

from idaclu import ida_shims


def get_func_prefs(func_name, is_uscore=False, is_dummy=False):
    prefs = []
    seen = set()
    dummy_pref = 'sub_'  # special prefix w/o '%'
    func_prfx = func_name.split('%')
    for p in func_prfx[:-1]:
        pref = '{}_'.format(p)
        if pref not in seen:
            prefs.append(pref)
            seen.add(pref)
    if is_dummy and dummy_pref in func_prfx[-1] and dummy_pref not in seen:
        prefs.append(dummy_pref)
    return prefs

def refresh_ui():
    ida_shims.refresh_idaview_anyway()
    widget = ida_shims.get_current_widget()
    widget_vdui = ida_shims.get_widget_vdui(widget)
    if widget_vdui:
        widget_vdui.refresh_ctext()

def create_folder(merge_name):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    func_dir.chdir('/')
    if not func_dir.isdir(merge_name):
        func_dir.mkdir(merge_name)
        return True
    return False

def graph_down(ea, path=set()):
    path.add(ea)
    call_instructions = []
    for address in idautils.FuncItems(ea):
        if not ida_shims.decode_insn(address):
            continue
        if not idaapi.is_call_insn(address):
            continue
        call_instructions.append(address)

    for x in call_instructions:
        for r in idautils.XrefsFrom(x, idaapi.XREF_FAR):
            if not r.iscode:
                continue
            func = idaapi.get_func(r.to)
            if not func:
                continue
            if (func.flags & (idaapi.FUNC_THUNK | idaapi.FUNC_LIB)) != 0:
                continue
            if r.to not in path:
                graph_down(r.to, path)
    return path

def recursive_prefix(addr):
    func_addr = ida_shims.get_name_ea(idaapi.BADADDR, ida_shims.get_func_name(addr))
    if func_addr == idaapi.BADADDR:
        ida_shims.msg("ERROR: function is not defined at 0x%08X\n" % addr)
        return
    nodes_xref_down = graph_down(func_addr, path=set([]))
    return nodes_xref_down

def get_nodes_edges(func_addr):
    func = idaapi.get_func(func_addr)
    g = idaapi.FlowChart(func)

    node_count = len(list(g))
    edge_count = 0
    for x in g:
        succ_count = len(list(x.succs()))
        pred_count = len(list(x.preds()))
        edge_count += (succ_count + pred_count)
    return (node_count, edge_count)

def get_func_ea_by_ref(func_ref):
    if isinstance(func_ref, int):
        return func_ref
    elif isinstance(func_ref, str):
        return idc.get_name_ea_simple(func_ref)
    elif isinstance(func_ref, func_t):
        return func_ref.start_ea

def get_func_item_eas(func_ref):
    func_ea = get_func_ea_by_ref(func_ref)
    for item_ea in list(idautils.FuncItems(func_ea)):
        if idaapi.is_code(ida_shims.get_full_flags(func_ea)):
            yield item_ea

def get_func_item_eas_once(func_ref):
    item_eas = []
    for ea in get_func_item_eas(func_ref):
        item_eas.append(ea)
    return item_eas

def get_func_set_attrs(fn_start=['sub_'], is_fn_start=True, attrs=['indx','addr','name', 'size', 'attr']):
    for func_idx, func_addr in enumerate(idautils.Functions()):
        func_name = ida_shims.get_func_name(func_addr)
        func_attr = idc.get_func_attr(func_addr, idc.FUNCATTR_FLAGS)
        func_desc = idaapi.get_func(func_addr)
        func_size = ida_shims.calc_func_size(func_desc)
        if any(func_name.startswith(pat) == is_fn_start for pat in fn_start):  # all ??
            attr_set = ()
            if 'indx' in attrs:
                attr_set += (func_idx,)
            if 'addr' in attrs:
                attr_set += (func_addr,)
            if 'name' in attrs:
                attr_set += (func_name,)
            if 'size' in attrs:
                attr_set += (func_size,)
            if 'attr' in attrs:
                attr_set += (func_attr,)
            yield attr_set


def is_function_solved(func_ref):
    EXPL_CALL_ARTS = [
        'call sub_',
        'call _',
        'call ds:',
        'call nullsub_',
        'call loc_',
        'call off_',
        'call j_j__',
        'call ??',
        ';',
        'jmp',
        'jz short sub_'
    ]

    func_ea = get_func_ea_by_ref(func_ref)
    item_eas = get_func_item_eas_once(func_ea)
    for item_idx, item_ea in enumerate(item_eas):
        if ida_shims.ua_mnem(item_ea) == 'call':
            item_dasm = idc.generate_disasm_line(item_ea, idaapi.GENDSM_FORCE_CODE)
            item_dasm_norm = ' '.join(item_dasm.split())
            if not any(item_dasm_norm.startswith(art) for art in EXPL_CALL_ARTS) and ' ; ' not in item_dasm:
                return False
    else:
        return True


def is_function_leaf(func_ref):
    func_ea = get_func_ea_by_ref(func_ref)
    item_eas = [item_ea for item_ea in get_func_item_eas(func_ea)]
    for item_ea in item_eas:
        if ida_shims.ua_mnem(item_ea) == 'call':
            return False
    else:
        if ida_shims.ua_mnem(item_eas[-1]) == 'jmp':
            return False
        else:
            return True  # until some "calling activity" is discovered inside,
                         # each function is considered as a "leaf"-function

def get_func_dirs(root_dir):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    ite = ida_dirtree.dirtree_iterator_t()

    s_folders = [root_dir]
    u_folders = [root_dir]

    while len(s_folders):
        curr_path = s_folders.pop()
        func_dir.chdir(curr_path)
        status = func_dir.findfirst(ite, "*")

        while status:
            entry_name = func_dir.get_entry_name(func_dir.resolve_cursor(ite.cursor))
            if func_dir.isdir(func_dir.get_abspath(ite.cursor)):
                current_dir_new = '{}/{}'.format('' if curr_path == '/' else curr_path, entry_name)
                s_folders.append(current_dir_new)
                if not current_dir_new in u_folders:
                    u_folders.append(current_dir_new)
            status = func_dir.findnext(ite)

    return u_folders

def get_dir_funcs(folders):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    ite = ida_dirtree.dirtree_iterator_t()
    idx = 0

    funcs = {}
    while idx < len(folders):
        curr_path = folders[idx]
        func_dir.chdir(curr_path)
        status = func_dir.findfirst(ite, "*")

        while status:
            entry_name = func_dir.get_entry_name(func_dir.resolve_cursor(ite.cursor))
            func_addr = ida_shims.get_name_ea(0, entry_name)
            if func_dir.isfile(func_dir.get_abspath(ite.cursor)):
                if curr_path != '/':
                    funcs[func_addr] = curr_path
            status = func_dir.findnext(ite)
        idx += 1

    return funcs

def get_func_name(func_ref):
    func_name = None
    if isinstance(func_ref, str):
        func_name = func_ref
    elif isinstance(func_ref, int):
        func_name = ida_shims.get_func_name(func_ref)
    else:
        raise ValueError("Invalid func reference")
    return func_name

def get_folder_norm(folder):
    return '' if folder == '/' else folder

def set_func_folder(func_ref, folder_src, folder_dst):
    func_name = get_func_name(func_ref)
    func_src = '{}/{}'.format(get_folder_norm(folder_src), func_name)
    func_dst = '{}/{}'.format(get_folder_norm(folder_dst), func_name)

    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    func_dir.chdir('/')
    func_dir.rename(func_src, func_dst)

def change_dir(dir):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    func_dir.chdir(dir)

idaclu/ida_utils.py
harlamism/IdaClu
import collections
import json
import os
import re
import sys
import time
#
import idc
import idaapi
import idautils
#
from idaclu import ida_shims
from idaclu.qt_shims import (
    QCoreApplication,
    QCursor,
    Qt,
    QtCore,
    QFrame,
    QIcon,
    QLineEdit,
    QMenu,
    QPushButton,
    QSize,
    QSizePolicy,
    QSpacerItem,
    QStyledItemDelegate,
    QVBoxLayout,
    QWidget
)
from idaclu import ida_utils
from idaclu import plg_utils
from idaclu.ui_idaclu import Ui_PluginDialog
from idaclu.qt_widgets import FrameLayout
from idaclu.models import ResultModel, ResultNode
from idaclu.assets import resource

# new backward-incompatible modules
try:
    import ida_dirtree
except ImportError:
    pass


class AppendTextEditDelegate(QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        editor = QLineEdit(parent)
        return editor

    def setEditorData(self, editor, index):
        current_text = index.data()
        editor.setText(current_text)

    def setModelData(self, editor, model, index):
        current_text = index.data()
        new_text = editor.text()
        appended_text = "{}".format(new_text)
        model.setData(index, appended_text)
        func_addr = ida_shims.get_name_ea(0, current_text)
        ida_shims.set_name(func_addr, new_text, idaapi.SN_NOWARN)


class IdaCluDialog(QWidget):
    def __init__(self, env_desc):
        super(IdaCluDialog, self).__init__()
        self.env_desc = env_desc
        self.ui = Ui_PluginDialog(env_desc)
        self.ui.setupUi(self)

        self.ui.rvTable.setItemDelegate(AppendTextEditDelegate())

        self.is_sidebar_on_left = True
        self.is_filters_shown = True
        self.option_sender = None
        self.is_mode_recursion = False
        # values to initialize the corresponding filter

        if self.env_desc.feat_folders:
            self.folders = ida_utils.get_func_dirs('/')
            self.folders_funcs = ida_utils.get_dir_funcs(self.folders)

        self.prefixes = self.getFuncPrefs(is_dummy=True)
        self.sel_dirs = []
        self.sel_prfx = []
        self.sel_colr = []
        self.rec_indx = {}

        self.heads = ['Name', 'Address', 'Size', 'Chunks', 'Nodes', 'Edges', 'Comment', 'Color']
        if env_desc.feat_folders:
            self.heads.insert(1, 'Folder')

        sp_path = self.get_splg_root(self.env_desc.plg_loc, 'idaclu')
        for frame in self.get_sp_controls(sp_path):
            self.ui.ScriptsContentsLayout.addWidget(frame)

        self.ui.wColorTool.setClickHandler(self.changeFuncColor)

        self.initFoldersFilter()
        self.initPrefixFilter()
        self.bindUiElems()

    def toggleRecursion(self):
        self.is_mode_recursion = not self.is_mode_recursion

    def bindUiElems(self):
        self.bindClicks()
        self.ui.rvTable.doubleClicked.connect(self.treeDoubleClick)
        self.ui.rvTable.customContextMenuRequested.connect(self.showContextMenu)

    def bindClicks(self):
        feat_folders = self.env_desc.feat_folders
        bind_data = [
            (self.ui.ScriptsHeader, self.swapPosition, True),
            (self.ui.FiltersHeader, self.showFilters, True)
        ]
        for (elem, meth, cond) in bind_data:
            if cond:
                elem.clicked.connect(meth)
        self.ui.wLabelTool.setModeHandler(self.toggleRecursion)
        self.ui.wLabelTool.setSetHandler(self.addLabel)
        self.ui.wLabelTool.setClsHandler(self.clsLabel)

    def getFuncPrefs(self, is_dummy=False):
        pfx_afacts = ['%', 'sub_']
        prefs = set()
        for func_addr in idautils.Functions():
            func_name = ida_shims.get_func_name(func_addr)
            if any(pa in func_name for pa in pfx_afacts):
                func_prefs = ida_utils.get_func_prefs(func_name, True, is_dummy)
                prefs.update(func_prefs)
        return list(prefs)

    def viewSelChanged(self):
        self.ui.wLabelTool.setEnabled(True)
        self.ui.wColorTool.setEnabled(True)

    def initPrefixFilter(self):
        self.ui.wPrefixFilter.addItems(self.prefixes)
        self.ui.wPrefixFilter.setText("")

    def initFoldersFilter(self):
        if self.env_desc.feat_folders:
            self.ui.wFolderFilter.addItems(self.folders)
            self.ui.wFolderFilter.setText("")
        else:
            self.ui.wFolderFilter.removeSelf()
            self.ui.FolderFilterLayout.setParent(None)
            layout = self.ui.vlFiltersGroup
            item = layout.takeAt(0)
            if item:
                widget = item.widget()
                if widget:
                    widget.deleteLater()
                del item

    def get_plugin_data(self):
        def sort_with_progress(constant, mcounter):
            def custom_sort(item):
                index, element = item
                mcounter[0] += 1
                finished = 65 + int(15 * (mcounter[0] / float(constant)))
                self.ui.wProgressBar.updateProgress(finished)
                return element['func_size']
            return custom_sort

        sender_button = self.sender()

        full_spec_name = sender_button.objectName()
        elem, cat, plg = full_spec_name.split('#')

        root_folder = self.env_desc.plg_loc
        module = None
        with plg_utils.PluginPath(os.path.join(root_folder, 'idaclu', 'plugins', cat)):
            module = __import__(plg)
            del sys.modules[plg]

        script_name = getattr(module, 'SCRIPT_NAME')
        script_type = getattr(module, 'SCRIPT_TYPE', 'custom')
        script_view = getattr(module, 'SCRIPT_VIEW', 'table')
        script_args = getattr(module, 'SCRIPT_ARGS', [])

        plug_params = {}
        if self.option_sender != None:
            widget = self.ui.ScriptsArea.findChild(QPushButton, self.option_sender)
            parent_layout = widget.parent().layout()

            if self.option_sender == full_spec_name:
                for i in range(parent_layout.count()):
                    sub_item = parent_layout.itemAt(i)
                    if sub_item:
                        sub_widget = sub_item.widget()
                        if sub_widget and (isinstance(sub_widget, QLineEdit)):
                            param_name = sub_widget.objectName().replace("{}__".format(full_spec_name), "")
                            plug_params[param_name] = sub_widget.text()  # .toPlainText()

            for i in range(parent_layout.count()):
                sub_item = parent_layout.itemAt(i)
                if sub_item:
                    if isinstance(sub_item, QSpacerItem):
                        parent_layout.removeItem(sub_item)
                        continue
                    sub_widget = sub_item.widget()
                    if sub_widget and (isinstance(sub_widget, QLineEdit)):
                        parent_layout.removeWidget(sub_widget)
                        sub_widget.setParent(None)

            self.option_sender = None

        elif self.option_sender == None and len(script_args) > 0:
            parent_widget = sender_button.parent()
            if parent_widget:
                for i, (ctrl_name, var_name, ctrl_ph) in enumerate(script_args):
                    text_edit = QLineEdit()
                    text_edit.setPlaceholderText(ctrl_ph)
                    text_edit.setMaximumSize(QSize(16777215, 30))
                    parent_widget.layout().addWidget(text_edit)
                    text_edit.setObjectName("{}__{}".format(full_spec_name, var_name))
                spacer = QSpacerItem(20, 30, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
                parent_widget.layout().addStretch(1)
                self.option_sender = full_spec_name
                return

        agroup = getattr(module, 'get_data')

        is_filter_embed = False
        if script_type == 'func':
            is_filter_embed = True
            sdata = agroup(self.updatePbFunc, self.env_desc, plug_params)  # pre-filter
        elif script_type == 'custom':
            is_filter_embed = False
            sdata = agroup(self.updatePb, self.env_desc, plug_params)  # post-filter
        else:
            ida_shims.msg('ERROR: Unknown plugin type')
            return
        sitems = None

        self.items = []

        sdatt = collections.defaultdict(list)
        overall_count = sum(len(lst) for lst in sdata.values())
        global_index = 0
        for dt in sdata:
            for tt in sdata[dt]:
                func_addr = None
                func_comm = None
                if isinstance(tt, int):
                    func_addr = tt
                    func_comm = ""
                elif self.env_desc.ver_py == 2 and isinstance(tt, long):
                    func_addr = int(tt)
                    func_comm = ""
                elif isinstance(tt, tuple):
                    func_addr = int(tt[0])
                    func_comm = tt[1]

                if is_filter_embed == False:
                    if not self.isFuncRelevant(func_addr):
                        continue

                node_count, edge_count = ida_utils.get_nodes_edges(func_addr)
                func_desc = idaapi.get_func(func_addr)
                func_name = ida_shims.get_func_name(func_addr)
                func_colr = ida_shims.get_color(func_addr, idc.CIC_FUNC)
                func_path = None
                if self.env_desc.feat_folders:
                    func_path = self.folders_funcs[func_addr] if func_addr in self.folders_funcs else '/'

                entry = collections.OrderedDict()
                entry['func_name'] = func_name
                if func_path:
                    entry['func_path'] = func_path
                entry['func_addr'] = hex(int(func_addr))
                entry['func_size'] = ida_shims.calc_func_size(func_desc)
                entry['func_chnk'] = len(list(idautils.Chunks(func_addr)))
                entry['func_node'] = node_count
                entry['func_edge'] = edge_count
                entry['func_comm'] = func_comm
                entry['func_colr'] = plg_utils.RgbColor(func_colr).get_to_str()

                sdatt[dt].append(entry)
                global_index += 1
                finished = 50 + int(15 * (global_index / float(overall_count)))
                self.ui.wProgressBar.updateProgress(finished)

        mut_counter = [0]
        for key, value in sdatt.items():
            sdatt[key] = sorted(enumerate(value), key=sort_with_progress(overall_count, mut_counter))

        global_index = 0
        for i, dt in enumerate(sdatt):
            self.items.append(ResultNode("{} ({})".format(dt, len(sdatt[dt]))))
            for j, (idx, tt) in enumerate(sdatt[dt]):
                self.items[-1].addChild(ResultNode(list(tt.values())))
                global_index += 1
                finished = 80 + int(15 * (global_index / float(overall_count)))
                self.rec_indx[int(tt['func_addr'], 16)] = (i, j)
                self.ui.wProgressBar.updateProgress(finished)


        self.some_options_shown = None
        self.ui.rvTable.setModel(ResultModel(self.heads, self.items, self.env_desc))
        self.ui.wProgressBar.updateProgress(100)
        self.prepareView()

    def prepareView(self):
        self.ui.rvTable.setColumnHidden(self.heads.index('Color'), True)
        # color component values; irrelevant
        rvTableSelModel = self.ui.rvTable.selectionModel()
        rvTableSelModel.selectionChanged.connect(self.viewSelChanged)
        self.ui.rvTable.header().resizeSection(0, 240)
        self.ui.rvTable.header().resizeSection(1, 96)
        self.ui.rvTable.header().resizeSection(2, 96)
        self.ui.rvTable.header().resizeSection(3, 96)

    def updatePb(self, curr_idx, total_count):
        finished = int(70 * (curr_idx / float(total_count)))
        self.ui.wProgressBar.updateProgress(finished)

    def updatePbFunc(self):
        self.sel_dirs = self.ui.wFolderFilter.getData()
        self.sel_prfx = self.ui.wPrefixFilter.getData()
        self.sel_colr = self.ui.wColorFilter.getSelectedColors()

        func_desc = list(idautils.Functions())
        func_count = len(func_desc)
        for func_idx, func_addr in enumerate(func_desc):

            if not self.isFuncRelevant(func_addr):
                continue

            finished = int(50 * (func_idx/float(func_count)))
            self.ui.wProgressBar.updateProgress(finished)
            yield func_addr

    def isFuncRelevant(self, func_addr):
        # function directories
        if len(self.sel_dirs) and self.sel_dirs[0] != '':
            if not (func_addr in self.folders_funcs and
                self.folders_funcs[func_addr] in self.sel_dirs):
                return False
        # function name prefixes
        func_name = ida_shims.get_func_name(func_addr)
        func_prfx = ida_utils.get_func_prefs(func_name, True, True)
        if len(self.sel_prfx) and self.sel_prfx[0] != '':
            if not any(p in self.sel_prfx for p in func_prfx):
                return False
        # function highlight color
        func_colr = plg_utils.RgbColor(ida_shims.get_color(func_addr, idc.CIC_FUNC))
        if len(self.sel_colr):
            if not any(func_colr == c for c in self.sel_colr):
                return False
        return True

    def treeDoubleClick(self, index):
        if not index.isValid():
            return None
        addr_index = index.sibling(index.row(), self.getFuncAddrCol())
        cell_data = addr_index.data()
        if cell_data and cell_data.startswith('0x'):
            idaapi.jumpto(plg_utils.from_hex(cell_data))

    def updateFilters(self, label_mode):
        label_name = None
        if label_mode == 'folder':
            label_name = self.ui.wLabelTool.getLabelName(prfx="/")
            self.ui.wFolderFilter.addItemNew(label_name)
        elif label_mode == 'prefix':
            label_name = self.ui.wLabelTool.getLabelName(sufx="_")
            self.ui.wPrefixFilter.addItemNew(label_name)
        return label_name

    def isDataSelected(self):
        return self.ui.rvTable.selectionModel().hasSelection()

    def addLabel(self):
        if self.isDataSelected():
            label_mode = self.ui.wLabelTool.getLabelMode()
            label_norm = self.updateFilters(label_mode)

            if self.env_desc.feat_folders and label_mode == 'folder':
                ida_utils.change_dir('/')
                ida_utils.create_folder(label_norm)

            addr_queue = self.getLabelAddrSet()
            for func_addr in addr_queue:
                func_name = ida_shims.get_func_name(func_addr)
                id_group, id_child = self.rec_indx[func_addr]
                id_col = self.heads.index('Address')
                indx_group = self.ui.rvTable.model().index(id_group, 0, QtCore.QModelIndex())
                indx_child = self.ui.rvTable.model().index(id_child, id_col, indx_group)
                if label_mode == 'prefix':
                    if not "{}%".format(label_norm[:-1]) in func_name:
                        func_name_new = plg_utils.add_prefix(func_name, label_norm, False)
                        ida_shims.set_name(func_addr, func_name_new, idaapi.SN_CHECK)
                        self.ui.rvTable.model().setData(indx_child, func_name_new)
                else:  # == 'folder'
                    folder_src = self.folders_funcs.get(func_addr, '/')
                    ida_utils.set_func_folder(func_addr, folder_src, label_norm)
                    self.ui.rvTable.model().setData(indx_child, label_norm)

            if self.env_desc.feat_folders:
                self.folders = ida_utils.get_func_dirs('/')
                self.folders_funcs = ida_utils.get_dir_funcs(self.folders)

            ida_utils.refresh_ui()

    def clsLabel(self):
        if self.ui.rvTable.selectionModel().hasSelection():
            indexes = [index for index in self.ui.rvTable.selectionModel().selectedRows()]
            data = [index.sibling(index.row(), self.getFuncAddrCol()).data() for index in indexes]
            for idx, addr_field in enumerate(data):
                func_addr = int(addr_field, base=16)
                func_name = ida_shims.get_func_name(func_addr)
                if self.ui.wLabelTool.getLabelMode() == 'prefix':
                    func_prefs = ida_utils.get_func_prefs(func_name, True, True)
                    if len(func_prefs) >= 1 and func_prefs[0] != 'sub_':
                        # get last prefix
                        name_token = str(func_prefs[0]).replace('_', '%')
                        func_name_new = func_name.replace(name_token, '')
                        ida_shims.set_name(func_addr, func_name_new, idaapi.SN_NOWARN)
                        self.ui.rvTable.model().setData(indexes[idx], func_name_new)
                elif self.ui.wLabelTool.getLabelMode() == 'folder':
                    func_fldr = self.folders_funcs.get(func_addr, '/')
                    ida_utils.set_func_folder(func_addr, func_fldr, '/')
                    self.ui.rvTable.model().setData(indexes[idx], '/')
                else:
                    ida_shims.msg('ERROR: unknown label mode')

    def showContextMenu(self, point):
        ix = self.ui.rvTable.indexAt(point)
        if ix.column() == 0:
            menu = QMenu()
            menu.addAction(QIcon(':/idaclu/icon_64.png'), "Rename")
            action = menu.exec_(self.ui.rvTable.mapToGlobal(point))
            if action:
                if action.text() == "Rename":
                    self.ui.rvTable.edit(ix)

    def getFuncAddrCol(self):
        if self.env_desc.feat_folders:
            return 2
        else:
            return 1

    def changeFuncColor(self):
        if self.isDataSelected():
            sender_button = self.sender()
            btn_name = sender_button.objectName()
            color = None
            if btn_name == 'SetColorBlue':
                color = plg_utils.RgbColor((199,255,255), 'blue')
            elif btn_name == 'SetColorYellow':
                color = plg_utils.RgbColor((255,255,191), 'yellow')
            elif btn_name == 'SetColorGreen':
                color = plg_utils.RgbColor((191,255,191), 'green')
            elif btn_name == 'SetColorPink':
                color = plg_utils.RgbColor((255,191,239), 'pink')
            elif btn_name == 'SetColorNone':
                color = plg_utils.RgbColor((255,255,255), 'white')
            else:
                ida_shims.msg('ERROR: unknown palette button')

            addr_queue = self.getLabelAddrSet()

            for func_addr in addr_queue:
                id_group, id_child = self.rec_indx[func_addr]
                id_col = self.heads.index('Address')
                indx_group = self.ui.rvTable.model().index(id_group, 0, QtCore.QModelIndex())
                indx_child = self.ui.rvTable.model().index(id_child, id_col, indx_group)
                ida_shims.set_color(func_addr, idc.CIC_FUNC, color.get_to_int())
                self.ui.rvTable.model().setData(indx_child, color.get_to_str())

            ida_utils.refresh_ui()

    def getLabelAddrSet(self):
        id_col = self.heads.index('Address')
        indexes = [idx for idx in self.ui.rvTable.selectionModel().selectedRows()]
        fields = [idx.sibling(idx.row(), id_col).data() for idx in indexes]

        addr_queue = set()
        for idx, field in enumerate(fields):
            func_addr = int(field, base=16)
            addr_queue.add(func_addr)

        addr_calees = set()
        if self.is_mode_recursion == True:
            for func_addr in addr_queue:
                addr_calees.update(ida_utils.recursive_prefix(func_addr))

        addr_queue.update(addr_calees)
        return addr_queue

    def swapPosition(self):
        layout = self.ui.DialogSplitter

        self.ui.SidebarFrame.setParent(None)
        self.ui.MainFrame.setParent(None)

        if not self.is_sidebar_on_left:
            layout.insertWidget(0, self.ui.SidebarFrame)
            layout.insertWidget(1, self.ui.MainFrame)
        else:
            layout.insertWidget(0, self.ui.MainFrame)
            layout.insertWidget(1, self.ui.SidebarFrame)

        layout.setCollapsible(0,False)
        layout.setCollapsible(1,False)

        self.is_sidebar_on_left = not self.is_sidebar_on_left

    def showFilters(self):
        if not self.is_filters_shown:
            self.ui.FiltersGroup.setMinimumSize(QSize(16777215, 16777215))
            self.ui.FiltersGroup.setMaximumSize(QSize(16777215, 16777215))
        else:
            self.ui.FiltersGroup.setMinimumSize(QSize(16777215, 1))
            self.ui.FiltersGroup.setMaximumSize(QSize(16777215, 1))

        self.is_filters_shown = not self.is_filters_shown

    def get_splg_root(self, plg_path, plg_fldr):
        splg_root = os.path.join(plg_path, plg_fldr, 'plugins')
        return splg_root

    def get_splg_tree(self, plg_splg_path):
        plg_tree = {}
        if os.path.exists(plg_splg_path):
            plg_tree = plg_utils.get_ordered_folder_tree(plg_splg_path)
        return plg_tree

    def is_sp_fname(self, sp_fname):
        return sp_fname.startswith('plugin_') and sp_fname.endswith('.py') and sp_fname != '__init__.py'

    def get_sp_controls(self, sp_path):
        sp_tree = self.get_splg_tree(sp_path)

        # depth of folder tree containing plugins is known
        for gdx, spg_ref in enumerate(sp_tree):
            if len(sp_tree[spg_ref]):
                spg_path = str(os.path.join(sp_path, spg_ref))
                spg_name = getattr(plg_utils.import_path(spg_path), 'PLUGIN_GROUP_NAME')
                spg_title = '{}. {}'.format(str(gdx+1), spg_name)

                spg_layout = FrameLayout(title=spg_title, env=self.env_desc)
                spg_layout.setProperty('class', 'frame')
                for sp_fname in sp_tree[spg_ref]:
                    plg_btn = None
                    if not self.is_sp_fname(sp_fname):
                        continue
                    sp_bname = sp_fname.replace('.py', '')
                    sp_name = sp_bname
                    # initial name is equal to file base name
                    # in case name will be not defined in plugin

                    sp_module = None
                    spe_msg = ""
                    # make sub-plugin discoverable in its group for importing
                    with plg_utils.PluginPath(os.path.join(sp_path, spg_ref)):
                        is_plug_ok = False
                        try:
                            sp_module = __import__(sp_bname)
                            del sys.modules[sp_bname]
                        except ImportError as err:
                            # in case some dependency is sub-plugin is missing
                            # the corresponding button will be disabled and
                            # tooltip will show this error
                            module_name = None
                            if self.env_desc.ver_py == 3:
                                module_name = err.name
                            else:
                                module_name = err.args[0].rsplit(' ',1)[-1]  # there is no .name attribute for Python2
                            spe_msg = "Module not found: {}".format(module_name)
                            # Attempt to open the module as a text file
                            # at least to recover sub-plugin name
                            try:
                                with open(os.path.join(sp_path, spg_ref, sp_fname), 'r') as file:
                                    for line in file:
                                        match = re.search(r'SCRIPT_NAME\s*=\s*["\']([^"\']+)', line)
                                        if match:
                                            sp_name = match.group(1)
                                            # self.log.debug("Recovered SCRIPT_NAME:", sp_name)
                                            break
                                    else:
                                        pass
                                        # self.log.debug("SCRIPT_NAME definition was not found")
                            except FileNotFoundError:
                                pass
                                # self.log.debug("Module file not found")
                        else:
                            is_plug_ok = True

                    # an attempt to load sub-plugin finished
                    # let's draw a corresponding button
                    sp_name = getattr(sp_module, 'SCRIPT_NAME', sp_name)
                    sp_layout = QVBoxLayout()
                    sp_frame = QFrame()
                    sp_frame.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
                    sp_frame.setObjectName('Frame#{}#{}'.format(spg_ref, sp_bname))

                    sp_button = QPushButton(sp_name)
                    if is_plug_ok:
                        sp_button.clicked.connect(self.get_plugin_data)
                    else:
                        sp_button.setEnabled(False)
                        sp_button.setToolTip(spe_msg)

                    sp_button.setObjectName('Button#{}#{}'.format(spg_ref, sp_bname))
                    sp_layout.addWidget(sp_button)
                    sp_frame.setLayout(sp_layout)
                    spg_layout.addWidget(sp_frame)
                yield spg_layout

idaclu/idaclu_gui.py
harlamism/IdaClu
from idaclu.qt_shims import (
    QAbstractItemModel,
    QBrush,
    QColor,
    QModelIndex,
    Qt
)


class ResultNode(object):
    def __init__(self, data):
        self._data = data
        if type(data) == tuple:
            self._data = list(data)
        if type(data) is str or not hasattr(data, '__getitem__'):
            self._data = [data]

        self._columncount = len(self._data)
        self._children = []
        self._parent = None
        self._row = 0

    def data(self, column):
        if column == 0:
            return str(self._data[column]).replace('%', '_')
        elif column >= 1 and column < len(self._data):
            return self._data[column]

    def columnCount(self):
        return self._columncount

    def childCount(self):
        return len(self._children)

    def child(self, row):
        if row >= 0 and row < self.childCount():
            return self._children[row]

    def parent(self):
        return self._parent

    def row(self):
        return self._row

    def addChild(self, child):
        child._parent = self
        child._row = len(self._children)
        self._children.append(child)
        self._columncount = max(child.columnCount(), self._columncount)

    def setData(self, column, value):
        if column < 0 or column >= len(self._data):
            return False
        self._data[column] = value
        return True


class ResultModel(QAbstractItemModel):

    def __init__(self, heads, nodes, env_desc):
        super(ResultModel, self).__init__()
        self._root = ResultNode(heads)
        self.env_desc = env_desc
        self.color_col = 8 if env_desc.lib_qt == 'pyqt5' else 7
        for node in nodes:
            self._root.addChild(node)

    def rowCount(self, index):
        if index.isValid():
            return index.internalPointer().childCount()
        return self._root.childCount()

    def addChild(self, node, _parent):
        if not _parent or not _parent.isValid():
            parent = self._root
        else:
            parent = _parent.internalPointer()
        parent.addChild(node)

    def index(self, row, column, _parent=None):
        if not _parent or not _parent.isValid():
            parent = self._root
        else:
            parent = _parent.internalPointer()

        if not QAbstractItemModel.hasIndex(self, row, column, _parent):
            return QModelIndex()

        child = parent.child(row)
        if child:
            return QAbstractItemModel.createIndex(self, row, column, child)
        else:
            return QModelIndex()

    def parent(self, index):
        if index.isValid():
            p = index.internalPointer().parent()
            if p:
                return QAbstractItemModel.createIndex(self, p.row(), 0, p)
        return QModelIndex()

    def columnCount(self, index):
        if index.isValid():
            return index.internalPointer().columnCount()
        return self._root.columnCount()

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        node = index.internalPointer()
        if role == Qt.DisplayRole:
            return node.data(index.column())

        elif role == Qt.BackgroundRole:
            node = index.internalPointer()
            rgb_string = node.data(self.color_col)
            if rgb_string and rgb_string != 'rgb(255,255,255)':
                rgb_values = rgb_string.replace("rgb(", "").replace(")", "")
                r, g, b = tuple(map(int, rgb_values.split(",")))
                color = QColor(r, g, b)
                if self.env_desc.lib_qt == 'pyqt5':
                    return color
                elif self.env_desc.lib_qt == 'pyside':
                    brush = QBrush(color)
                    return brush
        return None

    def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
        if role != Qt.EditRole or orientation != Qt.Horizontal:
            return False
        result = self._root.setData(section, value)
        if result:
            self.headerDataChanged.emit(orientation, section, section)
        return result

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._root.data(section)
        return None

    def flags(self, index):
        return Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsEditable

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        node = index.internalPointer()
        if role == Qt.EditRole:
            col = 1 if value.startswith('/') else 0
            if value.startswith('rgb'):
                col = self.color_col
            result = node.setData(col, value)
            if result:
                if self.env_desc.lib_qt == 'pyqt5':
                    if col == 8:
                        self.dataChanged.emit(index.sibling(index.row(), 0), index.sibling(index.row(), 7), [Qt.BackgroundRole])
                    else:
                        self.dataChanged.emit(index.sibling(index.row(), col), index.sibling(index.row(), col), [Qt.EditRole])
                elif self.env_desc.lib_qt == 'pyside':
                    if col == 7:
                        self.dataChanged.emit(index.sibling(index.row(), 0), [Qt.BackgroundRole])
                    else:
                        self.dataChanged.emit(index.sibling(index.row(), col), [Qt.EditRole])
            return True
        return False

idaclu/models.py
harlamism/IdaClu
import collections
import os
import re
import sys


class PluginPath():
    def __init__(self, path):
        self.path = path

    def __enter__(self):
        sys.path.insert(0, self.path)

    def __exit__(self, exc_type, exc_value, traceback):
        try:
            sys.path.remove(self.path)
        except ValueError:
            pass

class RgbColor:
    def __init__(self, color_ref, color_nam='unknown'):
        ver_py = sys.version_info.major
        if (ver_py == 2 and
            any(isinstance(color_ref, t) for t in (int, long)) and
            color_ref <= 0xFFFFFFFF):
            self.r, self.g, self.b = self.get_from_tuple(int(color_ref))
        elif (ver_py == 3 and
            isinstance(color_ref, int) and
            color_ref <= 0xFFFFFFFF):
            self.r, self.g, self.b = self.get_from_tuple(color_ref)
        elif isinstance(color_ref, tuple) and len(color_ref) == 3:
            self.r, self.g, self.b = color_ref
        elif isinstance(color_ref, str):
            self.r, self.g, self.b = self.get_from_str(color_ref)
        else:
            raise ValueError("Invalid init value")
        self.name = color_nam

    def get_from_tuple(self, rgb_int):
        r = (rgb_int >> 16) & 255
        g = (rgb_int >> 8) & 255
        b = rgb_int & 255
        return (r, g, b)

    def get_from_str(self, color_ref):
        rgb_pat = r'rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)'
        match = re.search(rgb_pat, color_ref)
        if match:
            r, g, b =  map(int, match.groups())
            if not all(0 <= c <= 255 for c in (r, g, b)):
                raise ValueError("Invalid color component values")
            return (r, g, b)
        else:
            raise ValueError("Invalid 'rgb(r,g,b)' string format")

    def get_to_str(self):
        return "rgb({},{},{})".format(self.r, self.g, self.b)

    def get_to_int(self):
        return (self.r << 16) + (self.g << 8) + self.b

    def __eq__(self, b):
        if isinstance(b, RgbColor):
            return self.r == b.r and self.g == b.g and self.b == b.b
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __str__(self):
        return self.get_to_str()

def from_hex(hex_sv):
    return int(hex_sv, base=16)

def invert_dict(original_dict):
    inverted_dict = {}
    for key, value in original_dict.items():
        if value not in inverted_dict:
            inverted_dict[value] = []
        inverted_dict[value].append(key)
    return inverted_dict

def add_prefix(func_name, func_pref, is_shadow=False):
    dlim_char = '_' if is_shadow else '%'
    pref_norm = func_pref.replace('_', '')
    func_name_new = '{}{}{}'.format(pref_norm, dlim_char, func_name)
    return func_name_new

def get_folder_tree(root_folder):
    folder_structure = {}
    for folder_name in os.listdir(root_folder):
        folder_path = os.path.join(root_folder, folder_name)
        if os.path.isdir(folder_path):
            folder_structure[folder_name] = get_folder_tree(folder_path)
        else:
            folder_structure[folder_name] = "file"
    return folder_structure

def get_ordered_folder_tree(root_folder):
    return collections.OrderedDict(sorted(get_folder_tree(root_folder).items()))

def import_path(path):
    strpath = str(path)
    parent_path = os.path.dirname(os.path.abspath(path))
    sys.path.append(parent_path)
    module = __import__(os.path.basename(path))
    sys.path.pop()
    return module

idaclu/plg_utils.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Xref Analysis'
idaclu/plugins/group_1/__init__.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Xref Count'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def sort_nat(input_dict):
    def fun(k, v):
        return [k, int(v)]

    def cmp_key(t):
        return fun(*re.match(r'([a-zA-Z\: ]+)(\d+)', t[0]).groups())

    return collections.OrderedDict(sorted(input_dict.items(), key=cmp_key))

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        func_xref_count = len(list(idautils.XrefsTo(func_addr)))

        xref_key = "xrefs: {}".format(func_xref_count)

        report['data'][xref_key].append(func_addr)
        report['stat'][xref_key] += 1

    report['data'] = sort_nat(report['data'])
    report['stat'] = sort_nat(report['stat'])
    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_1/plugin_1.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Xref Source'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_func_item_eas(func_addr):
    for item_ea in list(idautils.FuncItems(func_addr)):
        if ida_shims.is_code(ida_shims.get_full_flags(func_addr)):
            yield item_ea

def is_func_leaf(func_addr):
    item_eas = list(get_func_item_eas(func_addr))
    for item_ea in item_eas:
        if ida_shims.ua_mnem(item_ea) == 'call':
            return False
    else:
        if ida_shims.ua_mnem(item_eas[-1]) == 'jmp':
            return False
        else:
            return True

def is_func_expl(func_addr):
    call_afacts = [
        'call sub_',
        'call _',
        'call ds:',
        'call nullsub_',
        'call loc_',
        'call off_',
        'call j_',
        'call ??',
        ';',
        'jmp',
        'jz short sub_'
    ]

    for item_addr in get_func_item_eas(func_addr):
        if ida_shims.ua_mnem(item_addr) == 'call':

            dasm_flag = idaapi.GENDSM_FORCE_CODE
            dasm_line = ida_shims.generate_disasm_line(item_addr, dasm_flag)
            dasm_norm = ' '.join(dasm_line.split())

            if (not any(dasm_norm.startswith(art) for art in call_afacts) and
                ' ; ' not in dasm_line):
                return False
    else:
        return True

def get_func_type_src(func_addr):
    if is_func_leaf(func_addr):
        return 'leaf'
    elif is_func_expl(func_addr):
        return 'expl'
    else:
        return 'impl'

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        func_type = get_func_type_src(func_addr)

        report['data'][func_type].append(func_addr)
        report['stat'][func_type] += 1

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_1/plugin_2.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Xref Destination'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []

def is_fname_main(func_name):
    is_main = func_name.startswith('_') and 'main' in func_name.lower()
    return is_main

def is_func_lib(func_addr):
    func_name = ida_shims.get_func_name(func_addr)
    is_dummy = func_name.startswith('sub_')
    is_main = is_fname_main(func_name)
    func_flags = ida_shims.get_func_flags(func_addr)
    is_lib_flag = func_flags & idaapi.FUNC_LIB
    return is_lib_flag and not is_dummy and not is_main

def is_func_thunk(func_addr):
    func_flags = ida_shims.get_func_flags(func_addr)
    return func_flags & idaapi.FUNC_THUNK

def is_func_imp(func_addr):
    dasm_line = ida_shims.generate_disasm_line(func_addr, idaapi.GENDSM_FORCE_CODE)
    dasm_norm = ' '.join(dasm_line.split())
    return dasm_norm.startswith('jmp ds:__imp_')

def is_lib(func_addr):
    func_name = ida_shims.get_func_name(func_addr)
    if (is_func_lib(func_addr) or
        is_fname_lib(func_name, True) or
        ida_shims.get_segm_name(func_addr) == 'extern'):
        return True
    else:
        return False

def is_fname_lib(func_name, is_main=True):
    exclude_funcs = []
    if is_main == True:
        exclude_funcs.extend(['_main', '_wmain', '_WinMain@16'])

    if (not func_name in exclude_funcs) and \
        (
            func_name.startswith('_') or
            func_name.startswith('?') or
            func_name.startswith('unknown_libname_') or
            func_name.startswith('@_') or
            func_name.startswith('j_@_')
        ):
        return True
    else:
        return False

def is_fname_payload(func_name):
    lib_payload = [
        "`anonymous namespace'",
        "??1exception@boost@@MAE@XZ",
        "___std_parallel_algorithms_hw_threads@0",
        "?try_lock_for@stl_critical_section_concrt@details@Concurrency@@UAE_NI@Z",
        "?source_line@dbg_eng_data@?A0xbc047679@@QAEIQBX@Z"
    ]
    return any(func_name.startswith(x) for x in lib_payload)

def get_lib_prefixes():
    lib_prefix = [
        'lib_explicit_',
        'lib_implicit_',
        'j_lib_explicit_',
        'j_lib_implicit_'
    ]
    return lib_prefix

def is_fname_prefix(func_name):
    return any(func_name.startswith(p) for p in get_lib_prefixes())

def get_xref_addrs(func_addr):
    for xref in idautils.XrefsTo(func_addr):
        yield xref.frm

def get_cref_addrs(func_addr):
    for cref in idautils.CodeRefsTo(func_addr, 1):
        yield cref

def get_func_types_dst(func_addr):
    func_name = ida_shims.get_func_name(func_addr)
    func_type_dst = []

    for xref_addr in get_xref_addrs(func_addr):
        if ida_shims.is_code(ida_shims.get_full_flags(xref_addr)):
            xref_desc = idaapi.get_func(xref_addr)
            xref_name = ida_shims.get_func_name(xref_addr)

            if is_lib(xref_addr) and not is_fname_payload(xref_name):
                func_type_dst.append('lib_explicit')
            elif is_fname_prefix(xref_name):
                func_type_dst.append('lib_implicit')
            else:
                func_type_dst.append('payload')
        else:
            if ida_shims.get_segm_name(xref_addr) in ['.rdata', '.data']:
                func_type_dst.append('vftable')
            else:
                func_type_dst.append('unknown')

    return func_type_dst

def cleanup_lib_funcs(func_gen):
    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        if is_fname_prefix(func_name):
            lib_prefix = get_lib_prefixes()
            func_name = re.sub("|".join(lib_prefix), "", func_name)
            ida_shims.set_name(func_addr, func_name, idaapi.SN_NOWARN)

def discover_lib_funcs(func_gen):
    pass_count = 2

    for i in range(pass_count):
        for func_addr in func_gen():
            prefix = None
            func_name = ida_shims.get_func_name(func_addr)
            if is_fname_prefix(func_name):
                continue

            if is_lib(func_addr) or is_func_imp(func_addr) or is_fname_main(func_name):
                continue

            for xref_addr in get_cref_addrs(func_addr):
                xref_name = ida_shims.get_func_name(xref_addr)

                if is_fname_payload(xref_name):
                    continue

                if is_func_lib(xref_addr) or is_fname_lib(xref_name, True):
                    prefix = 'lib_explicit'
                    break
                elif any(xref_name.startswith(p) for p in get_lib_prefixes()):
                    prefix = 'lib_implicit'
                    break

            if prefix:
                ida_shims.set_name(func_addr, '{}_{}'.format(prefix, func_name), idaapi.SN_CHECK)

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    cleanup_lib_funcs(func_gen)
    discover_lib_funcs(func_gen)

    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        func_types_dst = []

        lib_prefix = get_lib_prefixes()
        func_name = re.sub("|".join(lib_prefix), "", func_name)
        if is_lib(func_addr) or is_func_imp(func_addr):
            continue

        func_types_dst = get_func_types_dst(func_addr)
        # unique types only
        func_types_dst = list(set(func_types_dst))

        func_type = None
        if len(func_types_dst) == 1:
            func_type = func_types_dst[0]
        else:

            if 'lib_explicit' in func_types_dst:
                func_type = 'lib_explicit'
            elif 'lib_implicit' in func_types_dst:
                func_type = 'lib_implicit'
            elif 'vftable' in func_types_dst:
                func_type = 'vftable'
            else:
                func_type = 'mix'

        report['data'][func_type].append(func_addr)
        report['stat'][func_type] += 1

    cleanup_lib_funcs(func_gen)
    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_1/plugin_3.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Constant Analysis'
idaclu/plugins/group_2/__init__.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
import idc
#
from idaclu import ida_shims


SCRIPT_NAME = 'Common Constants'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def sort_nat(input_dict):
    def fun(k, v):
        return [k, int(v)]

    def cmp_key(t):
        return fun(*re.match(r'([a-zA-Z\: ]+)(\d+)', t[0]).groups())

    return collections.OrderedDict(sorted(input_dict.items(), key=cmp_key))

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        func_desc = idaapi.get_func(func_addr)
        for head in idautils.Heads(ida_shims.start_ea(func_desc), ida_shims.end_ea(func_desc)):
            for opnd_index in range(idaapi.UA_MAXOP):
                opnd_type = ida_shims.get_operand_type(head, opnd_index)
                if opnd_type in [idaapi.o_void]:
                    break
                if opnd_type in [idaapi.o_reg, idaapi.o_phrase]:
                    opnd_str = ida_shims.print_operand(head, opnd_index)
                    if opnd_str in ['rsp', 'esp', 'rbp', 'ebp']:
                        break
                if opnd_type == idc.o_imm:
                    opnd_val = ida_shims.get_operand_value(head, opnd_index)
                    if ida_shims.is_loaded(opnd_val):
                        continue

                    # avoiding values that can be interpreted as addresses
                    # within the current sample:
                    #   SEH-handlers, callback-arguments, string-offsets, etc.

                    dasm_flag = idaapi.GENDSM_FORCE_CODE
                    dasm_line = ida_shims.generate_disasm_line(head, dasm_flag)
                    dasm_norm = ' '.join(dasm_line.split())
                    func_comm = '{} / {}'.format(hex(head), dasm_norm)

                    opnd_key = "const: {} / {}".format(opnd_val, hex(opnd_val))
                    report['data'][opnd_key].append((func_addr, func_comm))
                    report['stat'][opnd_key] += 1

    report['data'] = sort_nat(report['data'])
    report['stat'] = sort_nat(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_2/plugin_1.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
import idc
#
from idaclu import ida_shims


SCRIPT_NAME = 'String Refs'
SCRIPT_TYPE = 'custom'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def get_data(progress_callback=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    strs = list(idautils.Strings())
    strs_count = len(strs)

    for i, tstr in enumerate(strs):
        str_raw = ida_shims.get_strlit_contents(tstr)
        str_dec = str_raw.decode('utf-8', errors='replace').encode('ascii', errors='replace')

        for xref in idautils.XrefsTo(tstr.ea):
            xref_addr = xref.frm
            func_desc = idaapi.get_func(xref_addr)

            if func_desc:
                report['data'][str_dec].append(ida_shims.start_ea(func_desc))
                report['stat'][str_dec] += 1

        if progress_callback:
            progress_callback(i, strs_count)

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data()
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_2/plugin_2.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Global Variable Analysis'
SCRIPT_TYPE = 'custom'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def get_global_type(name_addr):
    flags = ida_shims.get_full_flags(name_addr)
    types = [
        'dword',
        'strlit',
        'char0',
        'byte',
        'word',
        'unknown',
        'struct',
        'float',
        'double',
        'enum0',
        'qword',
        'off0'
    ]
    for dt in types:
        is_data_type = getattr(ida_shims, 'is_{}'.format(dt))
        if is_data_type(flags):
            return dt
    return None

def get_data(progress_callback=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    names = list(idautils.Names())
    names_count = len(names)

    for i, (name_addr, name_line) in enumerate(names):
        g_type = get_global_type(name_addr)
        if g_type:
            for xref in idautils.XrefsTo(name_addr):
                xref_addr = xref.frm
                func_desc = idaapi.get_func(xref_addr)
                if func_desc:
                    name = "{} / {}".format(g_type, name_line)
                    func_addr = ida_shims.start_ea(func_desc)
                    if not func_addr in report['data'][name]:
                        report['data'][name].append(func_addr)
                        report['stat'][name] += 1

        if progress_callback:
            progress_callback(i, names_count)

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data()
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_2/plugin_3.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Lib Usage Analysis'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def is_fname_main(func_name):
    is_main = func_name.startswith('_') and 'main' in func_name.lower()
    return is_main

def is_func_lib(func_addr):
    func_name = ida_shims.get_func_name(func_addr)
    is_dummy = func_name.startswith('sub_')
    is_main = is_fname_main(func_name)
    func_flags = ida_shims.get_func_flags(func_addr)
    is_lib_flag = func_flags & idaapi.FUNC_LIB
    return is_lib_flag and not is_dummy and not is_main

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        if is_func_lib(func_addr):
            func_name = ida_shims.get_func_name(func_addr)
            xrefs = idautils.XrefsTo(func_addr)
            for xref in xrefs:
                xref_addr = xref.frm
                xref_desc = idaapi.get_func(xref_addr)

                if xref_desc:
                    report['data'][func_name].append(ida_shims.start_ea(xref_desc))
                    report['stat'][func_name] += 1

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_2/plugin_4.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'API Usage Analysis'
SCRIPT_TYPE = 'custom'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def get_idata():
        imports = {}
        module = ""

        def callback(ea, name, ordinal):
            imports[module].append((ea, name, ordinal))
            return True

        nimps = idaapi.get_import_module_qty()
        for i in range(0, nimps):
            module = idaapi.get_import_module_name(i)
            imports[module] = []
            idaapi.enum_import_names(i, callback)

        for mod in imports:
            for addr, name, ordi in imports[mod]:
                yield addr, mod, name, ordi


def get_data(progress_callback=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    imps = list(get_idata())
    imps_count = len(imps)

    for i, (addr, module, name, ordi) in enumerate(imps):
        for xref in idautils.XrefsTo(addr):
            xref_addr = xref.frm
            func_desc = idaapi.get_func(xref_addr)

            if func_desc:
                key_name = "{}_{}".format(module, name)
                report['data'][key_name].append(ida_shims.start_ea(func_desc))
                report['stat'][key_name] += 1

        if progress_callback:
            progress_callback(i, imps_count)

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data()
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_2/plugin_5.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Control Flow'
idaclu/plugins/group_3/__init__.py
harlamism/IdaClu
import collections
import json
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Control Flow Analysis'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_func_item_eas(func_addr):
    for item_ea in list(idautils.FuncItems(func_addr)):
        if ida_shims.is_code(ida_shims.get_full_flags(func_addr)):
            yield item_ea

def is_func_proxy(func_addr):
    item_eas = list(get_func_item_eas(func_addr))
    if ida_shims.ua_mnem(item_eas[-1]) == 'jmp':
        return True
    else:
        return False

def is_func_switch(func_addr):
    for (f_beg, f_end) in idautils.Chunks(func_addr):
        for head_ea in idautils.Heads(f_beg, f_end):
            if ida_shims.is_code(ida_shims.get_full_flags(head_ea)):
                si = ida_shims.get_switch_info(head_ea)
                if si == None:
                    continue
                results = idaapi.calc_switch_cases(head_ea, si)
                if results:
                    return True
                return False

def is_func_loop(func_desc):
    func_start_ea = ida_shims.start_ea(func_desc)

    blocks = [func_start_ea]
    for block in idaapi.FlowChart(func_desc):
        end_ea = ida_shims.end_ea(block)
        blocks.append(end_ea)

    for block in blocks:
        for xref in idautils.XrefsTo(block):
            xref_func = idaapi.get_func(xref.frm)
            xref_start_ea = ida_shims.start_ea(xref_func)

            if xref_func and xref_start_ea == func_start_ea:
                if xref.frm >= block:
                    return True
    return False


def is_func_simple_recursion(func_addr):
    for h in idautils.FuncItems(func_addr):
        for r in idautils.XrefsFrom(h, 0):
            if ((r.type == idaapi.fl_CF or r.type == idaapi.fl_CN) and
                r.to == func_addr):
                return True
    return False

stack = []
def is_func_recursion(func_addr):
    global stack
    if func_addr in stack:
        # print "This is recursive function", hex(func_addr), Name(func_addr)
        # for x in stack:
        #     print "\t", hex(x)
        # #insert your renaming here, it should be idc.MakeName
        return

    stack.append(func_addr)
    for h in idautils.FuncItems(func_addr):
        for r in idautils.XrefsFrom(h, 0):
            if ((r.type == idaapi.fl_CF or r.type == idaapi.fl_CN) and
                r.to != func_addr):
                is_func_recursion(r.to)
    stack = stack[:-1]

def is_func_condition(func_desc):
    bb_list = list(idaapi.FlowChart(func_desc))
    bb_num = len(bb_list)
    bb_conn_count = len(list(bb_list[0].succs()))
    if ((bb_num == 1 and bb_conn_count == 0) or
        (bb_num == 2 and bb_conn_count == 1 and is_func_proxy(ida_shims.start_ea(func_desc)))):
        return False
    return True

def get_data(func_gen=None, env_desc=None, plug_params=None):
    global stack

    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        func_desc = idaapi.get_func(func_addr)
        func_groups = []
        if is_func_proxy(func_addr):
            func_groups.append('proxy')

        is_switch = False
        if is_func_switch(func_addr):
            func_groups.append('switch')
            is_switch = True

        is_loop = False
        if is_func_loop(func_desc):
            func_groups.append('loop')
            is_loop = True

        if is_switch == False and is_loop == False:
            if is_func_condition(func_desc):
                func_groups.append('conditions')
            else:
                func_groups.append('simple')

        stack = []
        if is_func_simple_recursion(func_addr):
            if func_addr in stack:
                func_groups.append('simple_recursion')

        for g_name in func_groups:
            report['data'][g_name].append(func_addr)
            report['stat'][g_name] += 1

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_3/plugin_1.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Pseudocode Size'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def sort_nat(input_dict):
    def fun(k, v):
        return [k, int(v)]

    def cmp_key(t):
        return fun(*re.match(r'([a-zA-Z\: ]+)(\d+)', t[0]).groups())

    return collections.OrderedDict(sorted(input_dict.items(), key=cmp_key))

def get_psdo_list(func_ea):
    func_pseudocode = []
    decomp_str = ""
    try:
       decomp_str = idaapi.decompile(func_ea)
    except idaapi.DecompilationFailure:
       return []
    for line in str(decomp_str).split('\n'):
        if '//' in line:
            code = line.split('//')[0]
            if code != '':
                func_pseudocode.append(code.lstrip())
        else:
            if line != '':
                func_pseudocode.append(line.lstrip())
    return func_pseudocode
    
def get_psdo_body(func_ea):
    psdo_list = get_psdo_list(func_ea)
    return psdo_list[2:-1]

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        func_psdo_size = len(get_psdo_body(func_addr))
        key_name = "size: {}".format(func_psdo_size)
        report['data'][key_name].append(func_addr)
        report['stat'][key_name] += 1

    report['data'] = sort_nat(report['data'])
    report['stat'] = sort_nat(report['stat'])
    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_3/plugin_2.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Code Coverage'
idaclu/plugins/group_4/__init__.py
harlamism/IdaClu
#!/usr/bin/python
import os
import re
import sys
import mmap
import struct
import collections
from ctypes import *


#------------------------------------------------------------------------------
# DynamoRIO Drcov Log Parser
#------------------------------------------------------------------------------

class DrcovData(object):
    """
    A drcov log parser.
    """

    def __init__(self, filepath=None):
        self.filepath = filepath

        # drcov header attributes
        self.version = 0
        self.flavor = None

        # drcov module table
        self.module_table_count = 0
        self.module_table_version = 0
        self.modules = {}

        # drcov basic block data
        self.bbs = []
        self.bb_table_count = 0
        self.bb_table_is_binary = True

        # parse
        self._parse()

    #--------------------------------------------------------------------------
    # Public
    #--------------------------------------------------------------------------

    def get_offsets(self, module_name):
        """
        Return coverage data as basic block offsets for the named module.
        """
        modules = self.modules.get(module_name, [])
        if not modules:
            return []

        #
        # I don't know if this should ever actually trigger, but if it does,
        # it is a strange testcase to collect coverage against. It means that
        # maybe the target library/module was loaded, unloaded, and reloaded?
        #
        # if someone ever actally triggers this, we can look into it :S
        #

        if self.version > 2:
            assert all(module.containing_id == modules[0].id for module in modules)

        # extract the unique module ids that we need to collect blocks for
        mod_ids = [module.id for module in modules]

        # loop through the coverage data and filter out data for the target ids
        coverage_blocks = [bb.start for bb in self.bbs if bb.mod_id in mod_ids]

        # return the filtered coverage blocks
        return coverage_blocks

    def get_offset_blocks(self, module_name):
        """
        Return coverage data as basic blocks (offset, size) for the named module.
        """
        modules = self.modules.get(module_name, [])
        if not modules:
            return []

        # NOTE: see comment in get_offsets() for more info...
        if self.version > 2:
            assert all(module.containing_id == modules[0].id for module in modules)

        # extract the unique module ids that we need to collect blocks for
        mod_ids = [module.id for module in modules]

        # loop through the coverage data and filter out data for the target ids
        coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]

        # return the filtered coverage blocks
        return coverage_blocks

    #--------------------------------------------------------------------------
    # Parsing Routines - Top Level
    #--------------------------------------------------------------------------

    def _parse(self):
        """
        Parse drcov coverage from the given log file.
        """
        with open(self.filepath, "rb") as f:
            self._parse_drcov_header(f)
            self._parse_module_table(f)
            self._parse_bb_table(f)

    #--------------------------------------------------------------------------
    # Parsing Routines - Internals
    #--------------------------------------------------------------------------

    def _parse_drcov_header(self, f):
        """
        Parse drcov log header from filestream.
        """

        # parse drcov version from log
        #   eg: DRCOV VERSION: 2
        version_line = f.readline().decode('utf-8').strip()
        self.version = int(version_line.split(":")[1])

        # parse drcov flavor from log
        #   eg: DRCOV FLAVOR: drcov
        flavor_line = f.readline().decode('utf-8').strip()
        self.flavor = flavor_line.split(":")[1]

        assert self.version == 2, "Only drcov version 2 log files supported"

    def _parse_module_table(self, f):
        """
        Parse drcov log module table from filestream.
        """
        self._parse_module_table_header(f)
        self._parse_module_table_columns(f)
        self._parse_module_table_modules(f)

    def _parse_module_table_header(self, f):
        """
        Parse drcov log module table header from filestream.

        -------------------------------------------------------------------

        Format used in DynamoRIO v6.1.1 through 6.2.0
           eg: 'Module Table: 11'

        Format used in DynamoRIO v7.0.0-RC1 (and hopefully above)
           eg: 'Module Table: version X, count 11'

        """

        # parse module table 'header'
        #   eg: Module Table: version 2, count 11
        header_line = f.readline().decode('utf-8').strip()
        field_name, field_data = header_line.split(": ")
        #assert field_name == "Module Table"

        #
        # NOTE/COMPAT:
        #
        #   DynamoRIO doesn't document their drcov log format, and it has
        #   changed its format at least once during its lifetime.
        #
        #   we just have to try parsing the table header one way to determine
        #   if its the old (say, a 'v1') table, or the new 'v2' table.
        #

        try:

            # seperate 'version X' and 'count Y' from each other ('v2')
            version_data, count_data = field_data.split(", ")

        # failure to unpack indicates this is an 'older, v1' drcov log
        except ValueError:
            self.module_table_count   = int(field_data)
            self.module_table_version = 1
            return

        # parse module table version out of 'version X'
        data_name, version = version_data.split(" ")
        #assert data_name == "version"
        self.module_table_version = int(version)
        if not self.module_table_version in [2, 3, 4, 5]:
            raise ValueError("Unsupported (new?) drcov log format...")

        # parse module count in table from 'count Y'
        data_name, count = count_data.split(" ")
        #assert data_name == "count"
        self.module_table_count = int(count)

    def _parse_module_table_columns(self, f):
        """
        Parse drcov log module table columns from filestream.

        -------------------------------------------------------------------

        DynamoRIO v6.1.1, table version 1:
           eg: (Not present)

        DynamoRIO v7.0.0-RC1, table version 2:
           Windows:
             'Columns: id, base, end, entry, checksum, timestamp, path'
           Mac/Linux:
             'Columns: id, base, end, entry, path'

        DynamoRIO v7.0.17594B, table version 3:
           Windows:
             'Columns: id, containing_id, start, end, entry, checksum, timestamp, path'
           Mac/Linux:
             'Columns: id, containing_id, start, end, entry, path'

        DynamoRIO v7.0.17640, table version 4:
           Windows:
             'Columns: id, containing_id, start, end, entry, offset, checksum, timestamp, path'
           Mac/Linux:
             'Columns: id, containing_id, start, end, entry, offset, path'

        """

        # NOTE/COMPAT: there is no 'Columns' line for the v1 table...
        if self.module_table_version == 1:
            return

        # parse module table 'columns'
        #   eg: Columns: id, base, end, entry, checksum, timestamp, path
        column_line = f.readline().decode('utf-8').strip()
        field_name, field_data = column_line.split(": ")
        #assert field_name == "Columns"

        # seperate column names
        #   Windows:   id, base, end, entry, checksum, timestamp, path
        #   Mac/Linux: id, base, end, entry, path
        columns = field_data.split(", ")

    def _parse_module_table_modules(self, f):
        """
        Parse drcov log modules in the module table from filestream.
        """
        modules = collections.defaultdict(list)

        # loop through each *expected* line in the module table and parse it
        for i in range(self.module_table_count):
            module = DrcovModule(f.readline().decode('utf-8').strip(), self.module_table_version)
            modules[module.filename].append(module)

        self.modules = modules

    def _parse_bb_table(self, f):
        """
        Parse dcov log basic block table from filestream.
        """
        self._parse_bb_table_header(f)
        self._parse_bb_table_entries(f)

    def _parse_bb_table_header(self, f):
        """
        Parse drcov log basic block table header from filestream.
        """

        # parse basic block table 'header'
        #   eg: BB Table: 2792 bbs
        header_line = f.readline().decode('utf-8').strip()
        field_name, field_data = header_line.split(": ")
        #assert field_name == "BB Table"

        # parse basic block count out of 'X bbs'
        count_data, data_name = field_data.split(" ")
        #assert data_name == "bbs"
        self.bb_table_count = int(count_data)

        # peek at the next few bytes to determine if this is a binary bb table.
        # An ascii bb table will have the line: 'module id, start, size:'
        token = b"module id"
        saved_position = f.tell()

        # is this an ascii table?
        if f.read(len(token)) == token:
            self.bb_table_is_binary = False

        # nope! binary table
        else:
            self.bb_table_is_binary = True

        # seek back to the start of the table
        f.seek(saved_position)

    def _parse_bb_table_entries(self, f):
        """
        Parse drcov log basic block table entries from filestream.
        """

        # allocate the ctypes structure array of basic blocks
        self.bbs = (DrcovBasicBlock * self.bb_table_count)()

        # read binary basic block entries directly into the newly allocated array
        if self.bb_table_is_binary:
            f.readinto(self.bbs)

        # parse the plaintext basic block entries one by one
        else:
            self._parse_bb_table_text_entries(f)

    def _parse_bb_table_text_entries(self, f):
        """
        Parse drcov log basic block table text entries from filestream.
        """
        table_header = f.readline().decode('utf-8').strip()

        if table_header != "module id, start, size:":
            raise ValueError("Invalid BB header: %r" % table_header)

        pattern = re.compile(r"^module\[\s*(?P<mod>[0-9]+)\]\:\s*(?P<start>0x[0-9a-fA-F]+)\,\s*(?P<size>[0-9]+)$")
        for i, bb in enumerate(self.bbs):
            text_entry = f.readline().decode('utf-8').strip()
            if not text_entry:
                continue

            match = pattern.match(text_entry)
            if not match:
                raise ValueError("Invalid BB entry: %r" % text_entry)

            bb.start = int(match.group("start"), 16)
            bb.size = int(match.group("size"), 10)
            bb.mod_id = int(match.group("mod"), 10)

#------------------------------------------------------------------------------
# drcov module parser
#------------------------------------------------------------------------------

class DrcovModule(object):
    """
    Parser & wrapper for module details as found in a drcov coverage log.

    A 'module' in this context is a .EXE, .DLL, ELF, MachO, etc.
    """
    def __init__(self, module_data, version):
        self.id    = 0
        self.base  = 0
        self.end   = 0
        self.size  = 0
        self.entry = 0
        self.checksum  = 0
        self.timestamp = 0
        self.path      = ""
        self.filename  = ""
        self.containing_id = 0

        # parse the module
        self._parse_module(module_data, version)

    @property
    def start(self):
        """
        Compatability alias for the module base.

        DrCov table version 2 --> 3 changed this paramter name base --> start.
        """
        return self.base

    def _parse_module(self, module_line, version):
        """
        Parse a module table entry.
        """
        data = module_line.split(", ")

        # NOTE/COMPAT
        if version == 1:
            self._parse_module_v1(data)
        elif version == 2:
            self._parse_module_v2(data)
        elif version == 3:
            self._parse_module_v3(data)
        elif version == 4:
            self._parse_module_v4(data)
        elif version == 5:
            self._parse_module_v5(data)
        else:
            raise ValueError("Unknown module format (v%u)" % version)

    def _parse_module_v1(self, data):
        """
        Parse a module table v1 entry.
        """
        self.id       = int(data[0])
        self.size     = int(data[1])
        self.path     = str(data[2])
        self.filename = os.path.basename(self.path.replace('\\', os.sep))

    def _parse_module_v2(self, data):
        """
        Parse a module table v2 entry.
        """
        self.id        = int(data[0])
        self.base      = int(data[1], 16)
        self.end       = int(data[2], 16)
        self.entry     = int(data[3], 16)
        if len(data) == 7: # Windows Only
            self.checksum  = int(data[4], 16)
            self.timestamp = int(data[5], 16)
        self.path      = str(data[-1])
        self.size      = self.end-self.base
        self.filename  = os.path.basename(self.path.replace('\\', os.sep))

    def _parse_module_v3(self, data):
        """
        Parse a module table v3 entry.
        """
        self.id            = int(data[0])
        self.containing_id = int(data[1])
        self.base          = int(data[2], 16)
        self.end           = int(data[3], 16)
        self.entry         = int(data[4], 16)
        if len(data) > 7: # Windows Only
            self.checksum  = int(data[5], 16)
            self.timestamp = int(data[6], 16)
        self.path          = str(data[-1])
        self.size          = self.end-self.base
        self.filename      = os.path.basename(self.path.replace('\\', os.sep))

    def _parse_module_v4(self, data):
        """
        Parse a module table v4 entry.
        """
        self.id            = int(data[0])
        self.containing_id = int(data[1])
        self.base          = int(data[2], 16)
        self.end           = int(data[3], 16)
        self.entry         = int(data[4], 16)
        self.offset        = int(data[5], 16)
        if len(data) > 8: # Windows Only
            self.checksum  = int(data[6], 16)
            self.timestamp = int(data[7], 16)
        self.path          = str(data[-1])
        self.size          = self.end-self.base
        self.filename      = os.path.basename(self.path.replace('\\', os.sep))

    def _parse_module_v5(self, data):
        """
        Parse a module table v5 entry.
        """
        self.id            = int(data[0])
        self.containing_id = int(data[1])
        self.base          = int(data[2], 16)
        self.end           = int(data[3], 16)
        self.entry         = int(data[4], 16)
        self.offset        = int(data[5], 16)
        self.preferred_base= int(data[6], 16)
        if len(data) > 8: # Windows Only
            self.checksum  = int(data[7], 16)
            self.timestamp = int(data[8], 16)
        self.path          = str(data[-1])
        self.size          = self.end-self.base
        self.filename      = os.path.basename(self.path.replace('\\', os.sep))


#------------------------------------------------------------------------------
# drcov basic block parser
#------------------------------------------------------------------------------

class DrcovBasicBlock(Structure):
    """
    Parser & wrapper for basic block details as found in a drcov coverage log.

    NOTE:

      Based off the C structure as used by drcov -

        /* Data structure for the coverage info itself */
        typedef struct _bb_entry_t {
            uint   start;      /* offset of bb start from the image base */
            ushort size;
            ushort mod_id;
        } bb_entry_t;

    """
    _pack_   = 1
    _fields_ = [
        ('start',  c_uint32),
        ('size',   c_uint16),
        ('mod_id', c_uint16)
    ]

idaclu/plugins/group_4/drcov.py
harlamism/IdaClu
import bisect
import collections

import idautils
import idaapi
from idaclu import ida_shims
import drcov


SCRIPT_NAME = 'Covered Functions'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = [('filePath', 'file_path', 'input the file path')]


def find_function(block_start, function_ranges):
    index = bisect.bisect_right(function_ranges, (block_start, block_start))

    if index > 0 and block_start <= function_ranges[index - 1][1]:
        return function_ranges[index - 1]
    return None


def get_data(func_gen=None, env_desc=None, plug_params=None):
    REPORT = {
        'data': {},
        'stat': {},
        'tree': [
            {'unique_id': 1, 'parent_id': 0, 'Function': '', 'VA': ' ', 'Size': ' '}
        ]
    }
    raw_data = {}
    unseen_functions = []
    seen_functions = []

    function_ranges = []
    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        func_desc = idaapi.get_func(func_addr)
        func_size = ida_shims.calc_func_size(func_desc)
        function_ranges.append((func_addr, func_addr + func_size))
        unseen_functions.append(func_addr)

    x = None
    file_path = plug_params['file_path']
    try:
        x = drcov.DrcovData(file_path)
    except IOError:
        ida_shims.msg("ERROR: Cannot open coverage file: {}".format(file_path))
        return REPORT['data']

    coverage_blocks = x.get_offset_blocks(env_desc.ida_module)
    imagebase = idaapi.get_imagebase()

    for bb in coverage_blocks:
        block_start = imagebase + bb[0]
        func_start = find_function(block_start, function_ranges)
        if func_start:
            if not func_start[0] in seen_functions:
                seen_functions.append(func_start[0])

    group_name = 'covered'
    if not group_name in raw_data:
        raw_data[group_name] = []
        REPORT['stat'][group_name] = 0

    for sf in seen_functions:
        raw_data[group_name].append(sf)
        REPORT['stat'][group_name] += 1

    unseen = list(set(unseen_functions) - set(seen_functions))
    group_name = 'uncovered'
    if not group_name in raw_data:
        raw_data[group_name] = []
        REPORT['stat'][group_name] = 0

    for uf in unseen:
        raw_data[group_name].append(uf)
        REPORT['stat'][group_name] += 1


    REPORT['data'] = collections.OrderedDict(sorted(raw_data.items()))
    return REPORT['data']


def debug():
    print(json.dumps(get_data(), indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_4/plugin_1.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Function Similarity'
idaclu/plugins/group_5/__init__.py
harlamism/IdaClu
import collections
import json
#
import ssdeep
#
import idaapi
import idautils
#
from idaclu import ida_shims

SCRIPT_NAME = 'SSDEEP Similarity'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_items(func_ea):
    for item in idautils.FuncItems(func_ea):
        if ida_shims.is_code(ida_shims.get_full_flags(item)):
            yield item

def get_dasm_list(func_ea):
    func_instructs = []
    for item in get_items(func_ea):
        dasm = ida_shims.generate_disasm_line(item, idaapi.GENDSM_FORCE_CODE)
        dasm_clean = dasm.split(';')[0]  # remove comments
        func_instructs.append(dasm_clean)
    return func_instructs

def get_mnem_list(func_ea):
    func_mnemonics = []
    for item in get_items(func_ea):
        mnem = ida_shims.print_insn_mnem(item)
        func_mnemonics.append(mnem)
    return func_mnemonics

def get_psdo_list(func_ea):
    func_pseudocode = []
    decomp_str = ""
    try:
        decomp_str = idaapi.decompile(func_ea)
    except idaapi.DecompilationFailure:
        return []
    for line in str(decomp_str).split('\n'):
        if '//' in line:
            code = line.split('//')[0]
            if code != '':
                func_pseudocode.append(code.lstrip())
        else:
            if line != '':
                func_pseudocode.append(line.lstrip())
    return func_pseudocode

def get_psdo_body(func_ea):
    psdo_list = get_psdo_list(func_ea)
    return psdo_list[2:-1]

def get_func_bytes(func_addr):
    func_bytes = b''
    for beg, end in idautils.Chunks(func_addr):
        fb = ida_shims.get_bytes(beg, end-beg)
        func_bytes += fb
    return func_bytes

def get_number_ratio(num1, num2):
    if num1 == num2:
        return 1
    if num1 > num2:
        return num2 / num1
    if num1 < num2:
        return num1 / num2

def get_func_descriptors(func_gen):
    func_dscs = []
    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        func_desc = idaapi.get_func(func_addr)

        func_inst = get_dasm_list(func_addr)
        func_mnem = get_mnem_list(func_addr)
        func_psdo = get_psdo_list(func_addr)
        func_size = ida_shims.calc_func_size(func_desc)

        func_byts_line = get_func_bytes(func_addr)    # get_bytes(func_addr, func_size)
        func_mnem_line = "@".join(func_mnem).encode('utf-8', errors='replace')
        func_inst_line = "@".join(func_inst).encode('utf-8', errors='replace')
        func_psdo_line = "@".join(func_psdo).encode('utf-8', errors='replace')

        func_byts_size = len(func_byts_line)
        func_mnem_size = len(func_mnem_line)
        func_inst_size = len(func_inst_line)
        func_psdo_size = len(func_psdo_line)

        ssdeep_byts = ssdeep.hash(func_byts_line)
        ssdeep_mnem = ssdeep.hash(func_mnem_line)
        ssdeep_inst = ssdeep.hash(func_inst_line)
        ssdeep_psdo = ssdeep.hash(func_psdo_line)

        func_dscs.append({
            # general
            'func_addr': func_addr,
            'func_name': func_name,
            # ssdeep
            'byts_hash': ssdeep_byts,
            'mnem_hash': ssdeep_mnem,
            'inst_hash': ssdeep_inst,
            'psdo_hash': ssdeep_psdo,
            # sizes
            'byts_size': func_byts_size,
            'mnem_size': func_mnem_size,
            'inst_size': func_inst_size,
            'psdo_size': func_psdo_size
        })

    return func_dscs

def get_func_clusters(func_descriptors):
    clusters = collections.defaultdict(list)
    data_type = [('byts', 48), ('mnem', 80), ('inst', 80), ('psdo', 80)]
    for idx, sup in enumerate(func_descriptors):
        for jdx, sub in enumerate(func_descriptors):
            if sup['func_addr'] != sub['func_addr']:

                byts_score = None

                for (dt, th) in data_type:
                    size_key = '{}_size'.format(dt)
                    hash_key = '{}_hash'.format(dt)
                    if get_number_ratio(sup[size_key], sub[size_key]) > 0.333:
                        score = ssdeep.compare(sup[hash_key], sub[hash_key])
                        # pairs only
                        if score and score > th:
                            clusters[dt].append({
                                'score': score, 
                                'func_1': sup['func_addr'], 
                                'func_2': sub['func_addr']}
                            )

                            cluster_id = -1  # there is no recipient cluster for a given pair
                            for i, clu in enumerate(clusters['aggregated']):  # enumerating already existing clusters
                                if sup['func_addr'] in clu or sub['func_addr'] in clu:
                                    cluster_id = i

                            if cluster_id != -1:  # was found, but need more details
                                if sup['func_addr'] not in clusters['aggregated'][cluster_id]:
                                    clusters['aggregated'][cluster_id].append(sup['func_addr'])
                                elif sub['func_addr'] not in clusters['aggregated'][cluster_id]:
                                    clusters['aggregated'][cluster_id].append(sub['func_addr'])
                                else:
                                    pass
                            else:  # nothing similar was found
                                clusters['aggregated'].append([sup['func_addr'], sub['func_addr']])
    return clusters

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    func_descriptors = get_func_descriptors(func_gen)
    func_clusters = get_func_clusters(func_descriptors)


    for idx, clu in enumerate(func_clusters['aggregated']):
        for addr in clu:
            key_name = "cluster: {}".format(idx)
            report['data'][key_name].append(addr)
            report['stat'][key_name] += 1


    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_5/plugin_1.py
harlamism/IdaClu
import collections
import json
#
import tlsh
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'TLSH Similarity'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_items(func_ea):
    for item in idautils.FuncItems(func_ea):
        if ida_shims.is_code(ida_shims.get_full_flags(item)):
            yield item

def get_dasm_list(func_ea):
    func_instructs = []
    for item in get_items(func_ea):
        dasm = ida_shims.generate_disasm_line(item, idaapi.GENDSM_FORCE_CODE)
        dasm_clean = dasm.split(';')[0]  # remove comments
        func_instructs.append(dasm_clean)
    return func_instructs

def get_mnem_list(func_ea):
    func_mnemonics = []
    for item in get_items(func_ea):
        mnem = ida_shims.print_insn_mnem(item)
        func_mnemonics.append(mnem)
    return func_mnemonics

def get_psdo_list(func_ea):
    func_pseudocode = []
    decomp_str = ""
    try:
        decomp_str = idaapi.decompile(func_ea)
    except idaapi.DecompilationFailure:
        return []
    for line in str(decomp_str).split('\n'):
        if '//' in line:
            code = line.split('//')[0]
            if code != '':
                func_pseudocode.append(code.lstrip())
        else:
            if line != '':
                func_pseudocode.append(line.lstrip())
    return func_pseudocode

def get_psdo_body(func_ea):
    psdo_list = get_psdo_list(func_ea)
    return psdo_list[2:-1]

def get_func_bytes(func_addr):
    func_bytes = b''
    for beg, end in idautils.Chunks(func_addr):
        fb = ida_shims.get_bytes(beg, end-beg)
        func_bytes += fb
    return func_bytes

def get_number_ratio(num1, num2):
    if num1 == num2:
        return 1
    if num1 > num2:
        return num2 / num1
    if num1 < num2:
        return num1 / num2

def get_func_descriptors(func_gen):
    func_dscs = []
    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        func_desc = idaapi.get_func(func_addr)

        func_inst = get_dasm_list(func_addr)
        func_mnem = get_mnem_list(func_addr)
        func_psdo = get_psdo_list(func_addr)
        func_size = ida_shims.calc_func_size(func_desc)

        func_byts_line = get_func_bytes(func_addr)    # get_bytes(func_addr, func_size)
        func_mnem_line = "@".join(func_mnem).encode('utf-8', errors='replace')
        func_inst_line = "@".join(func_inst).encode('utf-8', errors='replace')
        func_psdo_line = "@".join(func_psdo).encode('utf-8', errors='replace')

        func_byts_size = len(func_byts_line)
        func_mnem_size = len(func_mnem_line)
        func_inst_size = len(func_inst_line)
        func_psdo_size = len(func_psdo_line)

        tlsh_byts = tlsh.hash(func_byts_line)
        tlsh_mnem = tlsh.hash(func_mnem_line)
        tlsh_inst = tlsh.hash(func_inst_line)
        tlsh_psdo = tlsh.hash(func_psdo_line)

        func_dscs.append({
            # general
            'func_addr': func_addr,
            'func_name': func_name,
            # tlsh
            'byts_hash': tlsh_byts,
            'mnem_hash': tlsh_mnem,
            'inst_hash': tlsh_inst,
            'psdo_hash': tlsh_psdo,
            # sizes
            'byts_size': func_byts_size,
            'mnem_size': func_mnem_size,
            'inst_size': func_inst_size,
            'psdo_size': func_psdo_size
        })

    return func_dscs

def get_func_clusters(func_descriptors):
    clusters = collections.defaultdict(list)
    data_type = [('byts', 100), ('mnem', 60), ('inst', 60), ('psdo', 60)]
    for idx, sup in enumerate(func_descriptors):
        for jdx, sub in enumerate(func_descriptors):
            if sup['func_addr'] != sub['func_addr']:

                byts_score = None

                for (dt, th) in data_type:
                    size_key = '{}_size'.format(dt)
                    hash_key = '{}_hash'.format(dt)
                    if sup[hash_key] != "TNULL" and sub[hash_key] != "TNULL": 
                        score = tlsh.diff(sup[hash_key], sub[hash_key])
                        # pairs only
                        if score and score <= th:
                            clusters[dt].append({
                                'score': score, 
                                'func_1': sup['func_addr'], 
                                'func_2': sub['func_addr']}
                            )

                            cluster_id = -1  # there is no recipient cluster for a given pair
                            for i, clu in enumerate(clusters['aggregated']):  # enumerating already existing clusters
                                if sup['func_addr'] in clu or sub['func_addr'] in clu:
                                    cluster_id = i

                            if cluster_id != -1:  # was found, but need more details
                                if sup['func_addr'] not in clusters['aggregated'][cluster_id]:
                                    clusters['aggregated'][cluster_id].append(sup['func_addr'])
                                elif sub['func_addr'] not in clusters['aggregated'][cluster_id]:
                                    clusters['aggregated'][cluster_id].append(sub['func_addr'])
                                else:
                                    pass
                            else:  # nothing similar was found
                                clusters['aggregated'].append([sup['func_addr'], sub['func_addr']])
    return clusters

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    func_descriptors = get_func_descriptors(func_gen)
    func_clusters = get_func_clusters(func_descriptors)


    for idx, clu in enumerate(func_clusters['aggregated']):
        for addr in clu:
            key_name = "cluster: {}".format(idx)
            report['data'][key_name].append(addr)
            report['stat'][key_name] += 1


    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_5/plugin_2.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Virtual Functions'
idaclu/plugins/group_6/__init__.py
harlamism/IdaClu
import collections
import json
import re
#
import idautils
import idaapi
#
from idaclu import ida_shims


SCRIPT_NAME = 'Explicit Calls'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def get_psdo_list(func_ea):
    func_pseudocode = []
    decomp_str = ""
    try:
       decomp_str = idaapi.decompile(func_ea)
    except idaapi.DecompilationFailure:
       return []
    for line in str(decomp_str).split('\n'):
        if '//' in line:
            code = line.split('//')[0]
            if code != '':
                func_pseudocode.append(code.lstrip())
        else:
            if line != '':
                func_pseudocode.append(line.lstrip())
    return func_pseudocode
    
def get_psdo_body(func_ea):
    psdo_list = get_psdo_list(func_ea)
    return psdo_list[2:-1]
 
def get_data(func_gen=None, env_desc=None, plug_params=None):

    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        caller_name = idaapi.get_func_name(func_addr)
        caller_psdo = get_psdo_body(func_addr)

        for psdo_line in caller_psdo:
            is_func_matched = re.match('(?:(?:.*\s)?)([0-9a-zA-Z\_\:]+)\(.*\)(?:(?:.*)?)', psdo_line)  # (?:(?:.*\s)?)([0-9a-zA-Z\_\:]+)\(.*\)
            if is_func_matched:
                calee_name = is_func_matched.group(1)
                report['data'][calee_name].append((func_addr, psdo_line))
                report['stat'][calee_name] += 1

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_6/plugin_1.py
harlamism/IdaClu
import collections
import json
import re
#
import idautils
import idaapi
#
from idaclu import ida_shims


SCRIPT_NAME = 'Implicit Calls'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def order_item_len(input_dict):
    def get_len(val):
        fs = val[1]
        if isinstance(fs, int):
            return fs
        elif isinstance(fs, list):
            return len(fs)

    return collections.OrderedDict(sorted(input_dict.items(), key=get_len, reverse=True))

def get_psdo_list(func_ea):
    func_pseudocode = []
    decomp_str = ""
    try:
        decomp_str = idaapi.decompile(func_ea)
    except idaapi.DecompilationFailure:
        return []
    for line in str(decomp_str).split('\n'):
        if '//' in line:
            code = line.split('//')[0]
            if code != '':
                func_pseudocode.append(code.lstrip())
        else:
            if line != '':
                func_pseudocode.append(line.lstrip())
    return func_pseudocode

def get_psdo_body(func_ea):
    psdo_list = get_psdo_list(func_ea)
    return psdo_list[2:-1]

def remove_casts(call_str):
    call_res = call_str
    for m in re.finditer('\(\*(\([a-zA-Z0-9_\s\*\,\.\(\)]+\)\))\(', call_res):
        call_res = call_res.replace(m.group(1), '')
    call_res = re.sub(r"\(_.*\*\)", "", call_res)
    return call_res

def get_data(func_gen=None, env_desc=None, plug_params=None):

    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    for func_addr in func_gen():
        caller_name = idaapi.get_func_name(func_addr)
        caller_psdo = get_psdo_body(func_addr)

        psdo_size = len(caller_psdo)

        for i in range(psdo_size):
            psdo_line = caller_psdo[i]
            is_func_matched = re.match('(\(\*.*[A-Za-z0-9_]+\s\+\s[A-Za-z0-9_\s\*\)\+]+\)\))\(', psdo_line)
            if is_func_matched:
                call_raw = is_func_matched.group(1)
                call_fmt = remove_casts(call_raw)

                func_comm = psdo_line
                while ';' not in func_comm and i + 1 < psdo_size:
                    i += 1
                    if func_comm[-1] == ',':
                        func_comm += ' '
                    func_comm += caller_psdo[i]

                report['data'][call_fmt].append((func_addr, func_comm))
                report['stat'][call_fmt] += 1

    report['data'] = order_item_len(report['data'])
    report['stat'] = order_item_len(report['stat'])

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_6/plugin_2.py
harlamism/IdaClu
PLUGIN_GROUP_NAME = 'Filter Analysis'
idaclu/plugins/group_7/__init__.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
import ida_dirtree
#
from idaclu import ida_shims


SCRIPT_NAME = 'Distinct Folders'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_dirs(root_dir):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    ite = ida_dirtree.dirtree_iterator_t()

    s_folders = [root_dir]
    u_folders = [root_dir]

    while len(s_folders):
        curr_path = s_folders.pop()
        func_dir.chdir(curr_path)
        status = func_dir.findfirst(ite, "*")

        while status:
            entry_name = func_dir.get_entry_name(func_dir.resolve_cursor(ite.cursor))
            if func_dir.isdir(func_dir.get_abspath(ite.cursor)):
                current_dir_new = '{}/{}'.format('' if curr_path == '/' else curr_path, entry_name)
                s_folders.append(current_dir_new)
                if not current_dir_new in u_folders:
                    u_folders.append(current_dir_new)
            status = func_dir.findnext(ite)

    return u_folders

def get_dir_funcs(folders):
    func_dir = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
    ite = ida_dirtree.dirtree_iterator_t()
    idx = 0

    funcs = {}
    while idx < len(folders):
        curr_path = folders[idx]
        func_dir.chdir(curr_path)
        status = func_dir.findfirst(ite, "*")

        while status:
            entry_name = func_dir.get_entry_name(func_dir.resolve_cursor(ite.cursor))
            func_addr = ida_shims.get_name_ea(0, entry_name)
            if func_dir.isfile(func_dir.get_abspath(ite.cursor)):
                if curr_path != '/':
                    funcs[func_addr] = curr_path
            status = func_dir.findnext(ite)
        idx += 1

    return funcs


def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    dirs = get_dirs('/')
    dir_funcs = get_dir_funcs(dirs)

    for func_addr in func_gen():
        if func_addr in dir_funcs:
            dir_name = dir_funcs[func_addr]
            report['data'][dir_name].append(func_addr)
            report['stat'][dir_name] += 1

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_7/plugin_1.py
harlamism/IdaClu
import collections
import json
import re
#
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Distinct Prefixes'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


def get_func_prefs(func_name, is_uscore=False, is_dummy=False):
    prefs = set()
    dummy_pref = 'sub_'  # special prefix w/o '%'
    if is_dummy and dummy_pref in func_name:
        prefs.add(dummy_pref)
    func_prfx = func_name.split('%')[:-1]
    for p in func_prfx:
        prefs.add('{}_'.format(p))
    return list(prefs)

def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    sep_char = '%'
    for func_addr in func_gen():
        func_name = ida_shims.get_func_name(func_addr)
        prefs = set()

        if sep_char in func_name:
            func_prefs = get_func_prefs(func_name, True, False)
            prefs.update(func_prefs)
        for pfx in list(prefs):
            report['data'][pfx].append(func_addr)
            report['stat'][pfx] += 1

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_7/plugin_2.py
harlamism/IdaClu
import collections
import json
import re
import sys
#
import idc
import idaapi
import idautils
#
from idaclu import ida_shims


SCRIPT_NAME = 'Distinct Colors'
SCRIPT_TYPE = 'func'
SCRIPT_VIEW = 'tree'
SCRIPT_ARGS = []


class RgbColor:
    def __init__(self, color_ref, color_nam='unknown'):
        ver_py = sys.version_info.major
        if (ver_py == 2 and
            any(isinstance(color_ref, t) for t in (int, long)) and
            color_ref <= 0xFFFFFFFF):
            self.r, self.g, self.b = self.get_from_tuple(int(color_ref))
        elif (ver_py == 3 and
            isinstance(color_ref, int) and
            color_ref <= 0xFFFFFFFF):
            self.r, self.g, self.b = self.get_from_tuple(color_ref)
        elif isinstance(color_ref, tuple) and len(color_ref) == 3:
            self.r, self.g, self.b = color_ref
        elif isinstance(color_ref, str):
            self.r, self.g, self.b = self.get_from_str(color_ref)
        else:
            raise ValueError("Invalid init value")
        self.name = color_nam

    def get_from_tuple(self, rgb_int):
        r = (rgb_int >> 16) & 255
        g = (rgb_int >> 8) & 255
        b = rgb_int & 255
        return (r, g, b)

    def get_from_str(self, color_ref):
        rgb_pat = r'rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)'
        match = re.search(rgb_pat, color_ref)
        if match:
            r, g, b =  map(int, match.groups())
            if not all(0 <= c <= 255 for c in (r, g, b)):
                raise ValueError("Invalid color component values")
            return (r, g, b)
        else:
            raise ValueError("Invalid 'rgb(r,g,b)' string format")

    def get_to_str(self):
        return "rgb({},{},{})".format(self.r, self.g, self.b)

    def get_to_int(self):
        return (self.r << 16) + (self.g << 8) + self.b

    def __eq__(self, b):
        if isinstance(b, RgbColor):
            return self.r == b.r and self.g == b.g and self.b == b.b
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __str__(self):
        return self.get_to_str()


def get_data(func_gen=None, env_desc=None, plug_params=None):
    report = {
        'data': collections.defaultdict(list),
        'stat': collections.defaultdict(int)
    }

    colors = [
        RgbColor((199,255,255), 'blue'),
        RgbColor((255,255,191), 'yellow'),
        RgbColor((191,255,191), 'green'),
        RgbColor((255,191,239), 'pink'),
        RgbColor((255,255,255), 'white')
    ]

    for func_addr in func_gen():
        func_colr = ida_shims.get_color(func_addr, idc.CIC_FUNC)
        
        for color in colors:
            if RgbColor(func_colr) == color:
                color_name = color.name
                report['data'][color_name].append(func_addr)
                report['stat'][color_name] += 1

    return report if __name__ == '__main__' else report['data']

def debug():
    data_obj = get_data(func_gen=idautils.Functions)
    ida_shims.msg(json.dumps(data_obj, indent=4))

if __name__ == '__main__':
    debug()

idaclu/plugins/group_7/plugin_3.py
harlamism/IdaClu
# Shim file to support PySide v1.x and PyQt v5.x
# Documentation provided by Qt and Riverbank Computing Ltd.:
#  - https://srinikom.github.io/pyside-docs/
#  - https://doc.qt.io/qtforpython-5/
# Inspired by the gist of Willi Ballenthin:
#  - https://gist.github.com/williballenthin/277eedca569043ef0984


is_ida = True
try:
    import idaapi
except ImportError:
    is_ida = False


def get_DescendingOrder():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.Qt.SortOrder.DescendingOrder
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.Qt.DescendingOrder

def get_Signal():
    if is_ida and is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.Signal
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.pyqtSignal

def get_QAbstractItemModel():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QAbstractItemModel
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QAbstractItemModel

def get_QAbstractItemView():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QAbstractItemView
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QAbstractItemView

def get_QAction():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QAction
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QAction

def get_QApplication():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QApplication
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QApplication

def get_QBrush():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QBrush
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QBrush

def get_QByteArray():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QByteArray
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QByteArray

def get_QCheckBox():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QCheckBox
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QCheckBox

def get_QColor():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QColor
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QColor

def get_QComboBox():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QComboBox
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QComboBox

def get_QCompleter():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QCompleter
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QCompleter

def get_QCoreApplication():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QCoreApplication
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QCoreApplication

def get_QCursor():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QCursor
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QCursor

def get_QDialog():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QDialog
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QDialog

def get_QEvent():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QEvent
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QEvent

def get_QFont():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QFont
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QFont

def get_QFrame():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QFrame
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QFrame

def get_QGroupBox():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QGroupBox
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QGroupBox

def get_QHBoxLayout():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QHBoxLayout
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QHBoxLayout

def get_QIcon():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QIcon
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QIcon

def get_QImage():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QImage
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QImage

def get_QLabel():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QLabel
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QLabel

def get_QLineEdit():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QLineEdit
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QLineEdit


def get_QMainWindow():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QMainWindow
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QMainWindow

def get_QMenu():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QMenu
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QMenu

def get_QMessageBox():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QMessageBox
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QMessageBox

def get_QMetaObject():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QMetaObject
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QMetaObject

def get_QModelIndex():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QModelIndex
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QModelIndex

def get_QPainter():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QPainter
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QPainter

def get_QPixmap():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QPixmap
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QPixmap

def get_QPoint():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QPoint
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QPoint

def get_QPointF():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QPointF
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QPointF

def get_QProgressBar():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QProgressBar
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QProgressBar

def get_QPushButton():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QPushButton
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QPushButton

def get_QRadioButton():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QRadioButton
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QRadioButton

def get_QRect():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QRect
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QRect

def get_QScrollArea():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QScrollArea
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QScrollArea

def get_QSize():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QSize
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QSize

def get_QSizePolicy():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QSizePolicy
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QSizePolicy

def get_QSlider():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QSlider
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QSlider

def get_QSpacerItem():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QSpacerItem
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QSpacerItem

def get_QSplitter():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QSplitter
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QSplitter

def get_QStandardItem():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStandardItem
    else:
        import PyQt5.QtGui as QtGui
        return QtGui.QStandardItem

def get_QStringListModel():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStringListModel
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QStringListModel

def get_QStyle():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStyle
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QStyle

def get_QStyledItemDelegate():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStyledItemDelegate
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QStyledItemDelegate

def get_QStyleFactory():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStyleFactory
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QStyleFactory

def get_QStyleOptionComboBox():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStyleOptionComboBox
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QStyleOptionComboBox

def get_QStyleOptionSlider():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QStyleOptionSlider
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QStyleOptionSlider

def get_Qt():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.Qt
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.Qt

def get_QTableWidget():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTableWidget
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTableWidget

def get_QTableWidgetItem():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTableWidgetItem
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTableWidgetItem

def get_QTabWidget():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTabWidget
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTabWidget

def get_QtCore():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore
    else:
        import PyQt5.QtCore as QtCore
        return QtCore

def get_QTextBrowser():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTextBrowser
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTextBrowser

def get_QTextEdit():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTextEdit
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTextEdit

def get_QtGui():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui
    else:
        import PyQt5.QtGui as QtGui
        return QtGui

def get_QThread():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtCore as QtCore
        return QtCore.QThread
    else:
        import PyQt5.QtCore as QtCore
        return QtCore.QThread

def get_QTreeView():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTreeView
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTreeView

def get_QTreeWidget():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTreeWidget
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTreeWidget

def get_QTreeWidgetItem():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QTreeWidgetItem
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QTreeWidgetItem

def get_QtWidgets():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        return None
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets

def get_QVBoxLayout():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QVBoxLayout
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QVBoxLayout

def get_QWidget():
    if is_ida and idaapi.IDA_SDK_VERSION <= 680:
        import PySide.QtGui as QtGui
        return QtGui.QWidget
    else:
        import PyQt5.QtWidgets as QtWidgets
        return QtWidgets.QWidget


DescendingOrder = get_DescendingOrder()
Signal = get_Signal()

QAbstractItemModel = get_QAbstractItemModel()
QAbstractItemView = get_QAbstractItemView()
QAction = get_QAction()
QApplication = get_QApplication()
QBrush = get_QBrush()
QByteArray = get_QByteArray()
QCheckBox = get_QCheckBox()
QColor = get_QColor()
QComboBox = get_QComboBox()
QCompleter = get_QCompleter()
QCoreApplication = get_QCoreApplication()
QCursor = get_QCursor()
QDialog = get_QDialog()
QEvent = get_QEvent()
QFont = get_QFont()
QFrame = get_QFrame()
QGroupBox = get_QGroupBox()
QHBoxLayout = get_QHBoxLayout()
QIcon = get_QIcon()
QImage = get_QImage()
QLabel = get_QLabel()
QLineEdit = get_QLineEdit()
QMainWindow = get_QMainWindow()
QMenu = get_QMenu()
QMessageBox = get_QMessageBox()
QMetaObject = get_QMetaObject()
QModelIndex = get_QModelIndex()
QPainter = get_QPainter()
QPixmap = get_QPixmap()
QPoint = get_QPoint()
QPointF = get_QPointF()
QProgressBar = get_QProgressBar()
QPushButton = get_QPushButton()
QRadioButton = get_QRadioButton()
QRect = get_QRect()
QScrollArea = get_QScrollArea()
QSize = get_QSize()
QSizePolicy = get_QSizePolicy()
QSlider = get_QSlider()
QSpacerItem = get_QSpacerItem()
QSplitter = get_QSplitter()
QStandardItem = get_QStandardItem()
QStringListModel = get_QStringListModel()
QStyle = get_QStyle()
QStyledItemDelegate = get_QStyledItemDelegate()
QStyleFactory = get_QStyleFactory()
QStyleOptionComboBox = get_QStyleOptionComboBox()
QStyleOptionSlider = get_QStyleOptionSlider()
Qt = get_Qt()
QTableWidget = get_QTableWidget()
QTableWidgetItem = get_QTableWidgetItem()
QTabWidget = get_QTabWidget()
QtCore = get_QtCore()
QTextBrowser = get_QTextBrowser()
QTextEdit = get_QTextEdit()
QtGui = get_QtGui()
QThread = get_QThread()
QTreeView = get_QTreeView()
QTreeWidget = get_QTreeWidget()
QTreeWidgetItem = get_QTreeWidgetItem()
QtWidgets = get_QtWidgets()
QVBoxLayout = get_QVBoxLayout()
QWidget = get_QWidget()

idaclu/qt_shims.py
harlamism/IdaClu
from idaclu.qt_shims import (
    QCoreApplication,
)


def i18n(text, context="PluginDialog"):
    return QCoreApplication.translate(context, text)

idaclu/qt_utils.py
harlamism/IdaClu
# -*- coding: utf-8 -*-

from idaclu.qt_shims import (
    QComboBox,
    QColor,
    QCursor,
    QEvent,
    QFont,
    QFrame,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPainter,
    QPoint,
    QPointF,
    QProgressBar,
    QPushButton,
    QSize,
    QSizePolicy,
    QStandardItem,
    QStyledItemDelegate,
    Qt,
    QThread,
    QVBoxLayout,
    QWidget,
    Signal
)

from idaclu.qt_utils import i18n
from idaclu import plg_utils


class LabelTool(QWidget):
    PREFIX = 0
    FOLDER = 1

    def __init__(self, name, env_desc, parent=None):
        super(LabelTool, self).__init__(parent)

        self.env_desc = env_desc
        self.label_mode = LabelTool.PREFIX
        self.data = [
            {'caption': 'prefix', 'pholder': 'Insert name'},
            {'caption': 'folder', 'pholder': 'Insert name'}
        ]
        font = QFont()
        font.setBold(True)
        font.setWeight(75)
        self.font = font
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.initRecursionToggle(parent))
        layout.addLayout(self.initLabelEdit(parent))
        for actn in ['SetLabel', 'ClsLabel']:
            layout.addWidget(self.initActionButton(actn, parent))
        if self.env_desc.feat_folders:
            self._label.clicked.connect(self.toggleLabelMode)
        self.retranslateUi()
        self.setLayout(layout)
        self.decorateUi()


    def initActionButton(self, ref, parent):
        btn_name = "{}Button".format(ref)
        btn = QPushButton(parent)
        btn.setMinimumSize(QSize(75, 30))
        btn.setMaximumSize(QSize(75, 30))
        btn.setFont(self.font)
        btn.setEnabled(False)
        setattr(self, btn_name, btn)
        return btn

    def initRecursionToggle(self, parent):
        toggle = QPushButton(parent)
        toggle.setMinimumSize(QSize(30, 30))
        toggle.setMaximumSize(QSize(30, 30))
        toggle.setFont(self.font)
        toggle.setCheckable(True)
        toggle.setCursor(QCursor(Qt.PointingHandCursor))
        self._recur_toggle = toggle
        return toggle

    def initLabelEdit(self, parent):
        _PointingHandCursor = Qt.PointingHandCursor

        layout = QHBoxLayout()
        layout.setSpacing(0)

        label = QPushButton(parent)
        label.setCursor(QCursor(_PointingHandCursor))
        label.setMinimumSize(QSize(75, 30))
        label.setMaximumSize(QSize(75, 30))
        label.setFont(self.font)
        label.setCheckable(False)
        label.setAutoExclusive(False)
        self._label = label

        edit = QLineEdit(parent)
        edit.setMinimumSize(QSize(16777215, 30))
        edit.setMaximumSize(QSize(16777215, 30))
        edit.setPlaceholderText("Insert prefix")
        self._edit = edit

        layout.addWidget(self._label)
        layout.addWidget(self._edit)
        layout.setStretch(0, 2)
        layout.setStretch(1, 5)
        return layout

    def toggleLabelMode(self):
        self.label_mode = not self.label_mode
        caption = str(self.data[self.label_mode]['caption']).upper()
        pholder = self.data[self.label_mode]['pholder']
        self._label.setText(i18n(caption))
        self._edit.setPlaceholderText(i18n(pholder))

    def setEnabled(self, state):
        self.SetLabelButton.setEnabled(state)
        self.ClsLabelButton.setEnabled(state)

    def getLabelName(self, prfx='', sufx=''):
        text = self._edit.text()
        if not text.startswith(prfx):
            text = "{}{}".format(prfx, text)
        if not text.endswith(sufx):
            text = "{}{}".format(text, sufx)
        return text

    def getLabelMode(self):
        return self.data[self.label_mode]['caption']

    def setModeHandler(self, handler):
        self._recur_toggle.clicked.connect(handler)

    def setSetHandler(self, handler):
        self.SetLabelButton.clicked.connect(handler)

    def setClsHandler(self, handler):
        self.ClsLabelButton.clicked.connect(handler)

    def decorateUi(self):
        self._recur_toggle.setProperty('class','tool-btn tool-btn-hov')
        if self.env_desc.feat_folders:
            self._label.setProperty('class','tool-btn tool-btn-hov edit-head')
        else:
            self._label.setProperty('class','tool-btn edit-head')
            self._label.setCursor(QCursor(Qt.ArrowCursor))
        self.SetLabelButton.setProperty('class','tool-btn tool-btn-hov')
        self.ClsLabelButton.setProperty('class','tool-btn tool-btn-hov')

    def retranslateUi(self):
        self._recur_toggle.setText(i18n("R"))
        self._label.setText(i18n("PREFIX"))
        self.SetLabelButton.setText(i18n("ADD"))
        self.ClsLabelButton.setText(i18n("CLEAR"))
        self._recur_toggle.setToolTip(i18n("Toggle recursive mode on/off"))
        self._label.setToolTip(i18n("Switch between Prefix/Folder modes"))


class ProgressIndicator(QWidget):
    def __init__(self, name, parent=None):
        super(ProgressIndicator, self).__init__(parent)
        layout = QVBoxLayout()
        layout.addWidget(self.initProgressBar(parent))
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        self.setProgress(0)
        self._worker = Worker()
        self._worker.updateProgress.connect(self.setProgress)

    def initProgressBar(self, parent):
        progress = QProgressBar(parent)
        progress.setMinimumSize(QSize(0, 5))
        progress.setMaximumSize(QSize(16777215, 5))
        progress.setTextVisible(False)
        self._progress = progress
        return progress

    def setProgress(self, progress):
        if progress == 0:
            self.setVisible(False)
        elif progress == 100:
            self.setVisible(False)
            self._progress.setValue(0)
        else:
            self.setVisible(True)
            self._progress.setValue(progress)

    def updateProgress(self, progress):
        self._worker.updateProgress.emit(progress)


class ColorButton(QPushButton):
    def __init__(self, name, size=(30, 30), parent=None):
        QPushButton.__init__(self, parent=parent)
        self.setObjectName(name)
        self.setMinimumSize(QSize(*size))
        self.setMaximumSize(QSize(*size))
        self.setCheckable(True)
        self.setCursor(QCursor(Qt.PointingHandCursor))


class Worker(QThread):
    updateProgress = Signal(int)

    def __init__(self):
        QThread.__init__(self)

    def run(self):
        for i in range(1, 101):
            self.updateProgress.emit(i)
            # time.sleep(0.01)


class PaletteTool(QWidget):
    COLOR_NAME = 0
    COLOR_VAL = 1

    def __init__(self, name, size, pref, is_enbl=True, is_excl=True, parent=None):
        super(PaletteTool, self).__init__(parent)
        self.colors = [
            ("yellow", (255,255,191)),
            ("blue", (199,255,255)),
            ("green", (191,255,191)),
            ("pink", (255,191,239)),
            ("none", (255,255,255))
        ]

        self.setObjectName(name)
        self.layout = QHBoxLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)

        for name, _ in self.colors:
            btn_name = '{}{}'.format(pref, name.capitalize())
            btn_obj = ColorButton(u"{}".format(btn_name), size, parent)
            btn_obj.setProperty('class','plt-btn plt-btn-{}'.format(name))
            btn_obj.setText("")
            self.layout.addWidget(btn_obj)

        self.setEnabled(is_enbl)
        self.setAutoExclusive(is_excl)
        self.setLayout(self.layout)

    def setClickHandler(self, handler):
        for _, btn in self.enumButtons():
            btn.clicked.connect(handler)

    def setPrefix(self, pref):
        for _, btn in self.enumButtons():
            color = str(btn.objectName()).replace('SetColor', '').lower()
            btn.setToolTip("{} {}".format(pref, i18n(color)))

    def enumButtons(self):
        for i in range(self.layout.count()):
            widget = self.layout.itemAt(i).widget()
            if isinstance(widget, QPushButton):
                yield (i, widget)

    def setEnabled(self, state):
        for _, btn in self.enumButtons():
            btn.setEnabled(state)

    def setAutoExclusive(self, state):
        for _, btn in self.enumButtons():
            btn.setAutoExclusive(state)

    def getSelectedColors(self):
        colors = []
        for i, btn in self.enumButtons():
            if btn.isChecked():
                color_val = self.colors[i][PaletteTool.COLOR_VAL]
                colors.append(plg_utils.RgbColor(color_val))
        return colors


class FilterInputGroup(QWidget):
    def __init__(self, name, pholder, parent=None):
        super(FilterInputGroup, self).__init__(parent)

        self._items = []

        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.initText(name, self))
        layout.addWidget(self.initSelect(name))
        layout.setStretch(0, 0)
        layout.setStretch(1, 5)
        layout.setStretch(2, 7)
        layout.setStretch(3, 0)
        self.setPlaceholder(pholder)
        self.setLayout(layout)

    def initText(self, name, parent=None):
        font = QFont()
        font.setBold(True)
        font.setWeight(75)

        label = QPushButton(parent)
        label.setText(name.upper())
        label.setFont(font)
        label.setMinimumSize(QSize(96, 26))
        label.setMaximumSize(QSize(96, 26))
        label.setProperty('class', 'select-head')
        self._label = label
        return label

    def initSelect(self, name, parent=None):
        select = CheckableComboBox()
        select.setEnabled(True)
        select.setAutoFillBackground(False)
        select.setMinimumSize(QSize(16777215, 26))
        select.setMaximumSize(QSize(16777215, 26))
        select.lineEdit().setText("")
        self._select = select
        return select

    def setPlaceholder(self, pholder):
        self._select.lineEdit().setPlaceholderText(pholder)

    def addItems(self, items):
        self._items.extend(items)
        self._select.addItems(self._items)
        if len(self._items):
            self._select.setEnabled(True)
        else:
            self._select.setEnabled(False)

    def addItemNew(self, item):
        self._select.addItemNew(item)

    def setEnabled(self, state=False):
        self._select.setEnabled()

    def removeSelf(self):
        self._label.setParent(None)
        self._select.setParent(None)
        self.setParent(None)

    def setText(self, text):
        self._select.lineEdit().setText(text)

    def getData(self):
        return self._select.getData().split('; ')


class CheckableComboBox(QComboBox):
    def __init__(self):
        super(CheckableComboBox, self).__init__()
        self.setEditable(True)
        self.lineEdit().setReadOnly(True)
        self.closeOnLineEditClick = False
        self.lineEdit().installEventFilter(self)
        self.view().viewport().installEventFilter(self)
        self.model().dataChanged.connect(self.updateLineEditField)
        self.itemDelegate = QStyledItemDelegate(self)
        self.setItemDelegate(self.itemDelegate)

    def hidePopup(self):
        super(CheckableComboBox, self).hidePopup()
        self.startTimer(100)

    def addItems(self, items, itemList=None):
        for indx, text in enumerate(items):
            try:
                data = itemList[indx]
            except (TypeError, IndexError):
                data = None
            self.addItem(text, data)

    def addItemNew(self, text, userData=None):
        for row in range(self.model().rowCount()):
            item = self.model().item(row)
            if ((item and (item.text() == text)) or
                (userData and item.data() == userData)):
                return False
        self.addItem(text, userData)
        return True

    def addItem(self, text, userData=None):
        item = QStandardItem()
        item.setText(text)
        if not userData is None:
            item.setData(userData)
        item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
        item.setData(Qt.Unchecked, Qt.CheckStateRole)
        self.model().appendRow(item)

    def eventFilter(self, widget, event):
        if widget == self.lineEdit():
            if event.type() == QEvent.MouseButtonRelease:
                if self.closeOnLineEditClick:
                    self.hidePopup()
                else:
                    self.showPopup()
                return True
            return super(CheckableComboBox, self).eventFilter(widget, event)
        if widget == self.view().viewport():
            if event.type() == QEvent.MouseButtonRelease:
                indx = self.view().indexAt(event.pos())
                item = self.model().item(indx.row())

                if item.checkState() == Qt.Checked:
                    item.setCheckState(Qt.Unchecked)
                else:
                    item.setCheckState(Qt.Checked)
                return True
            return super(CheckableComboBox, self).eventFilter(widget, event)

    def updateLineEditField(self):
        text_container = []
        for i in range(self.model().rowCount()):
            if self.model().item(i).checkState() == Qt.Checked:
                text_container.append(self.model().item(i).text())
            text_string = '; '.join(text_container)
            self.lineEdit().setText(text_string)

    def getData(self):
        return self.lineEdit().text()

    def clearData(self):
        self.clear()


class FrameLayout(QWidget):
    def __init__(self, parent=None, title=None, env=None):
        self.env_desc = env
        QWidget.__init__(self, parent=parent)

        self._is_collasped = True
        self._title_frame = None
        self._content, self._content_layout = (None, None)

        title_frame = self.initTitleFrame(title, self._is_collasped)
        content_widget = self.initContent(self._is_collasped)

        self._main_v_layout = QVBoxLayout(self)
        self._main_v_layout.addWidget(title_frame)
        self._main_v_layout.addWidget(content_widget)

        self.initCollapsable()

    def initTitleFrame(self, title, collapsed):
        self._title_frame = self.TitleFrame(
            title=title,
            collapsed=collapsed,
            env=self.env_desc)
        return self._title_frame

    def initContent(self, collapsed):
        self._content = QWidget()
        self._content_layout = QVBoxLayout()

        self._content.setLayout(self._content_layout)
        self._content.setVisible(not collapsed)

        return self._content

    def addWidget(self, widget):
        self._content_layout.addWidget(widget)

    def initCollapsable(self):
        self._title_frame.clicked.connect(self.toggleCollapsed)

    def toggleCollapsed(self):
        self._content.setVisible(self._is_collasped)
        self._is_collasped = not self._is_collasped
        self._title_frame._arrow.setArrow(int(self._is_collasped))


    class TitleFrame(QFrame):

        clicked = Signal()
        def __init__(self, parent=None, title="", collapsed=False, env=None):
            QFrame.__init__(self, parent=parent)
            self.env_desc = env
            self.setMinimumHeight(24)
            self.move(QPoint(24, 0))

            self._hlayout = QHBoxLayout(self)
            self._hlayout.setContentsMargins(0, 0, 0, 0)
            self._hlayout.setSpacing(0)

            self._arrow = None
            self._title = None

            self._hlayout.addWidget(self.initArrow(collapsed))
            self._hlayout.addWidget(self.initTitle(title))

        def initArrow(self, collapsed):
            self._arrow = FrameLayout.Arrow(collapsed=collapsed, env=self.env_desc)
            return self._arrow

        def initTitle(self, title=None):
            self._title = QLabel(title)
            self._title.setMinimumHeight(24)
            self._title.move(QPoint(24, 0))

            return self._title

        def mousePressEvent(self, event):
            self.clicked.emit()
            return super(FrameLayout.TitleFrame, self).mousePressEvent(event)


    class Arrow(QFrame):
        def __init__(self, parent=None, collapsed=False, env=None):
            QFrame.__init__(self, parent=parent)
            self.env_desc = env
            self.setMaximumSize(24, 24)

            # horizontal == 0
            ha_point1 = QPointF(7.0, 8.0)
            ha_point2 = QPointF(17.0, 8.0)
            ha_point3 = QPointF(12.0, 13.0)
            self._arrow_horizontal = (ha_point1, ha_point2, ha_point3)
            # vertical == 1
            va_point1 = QPointF(8.0, 7.0)
            va_point2 = QPointF(13.0, 12.0)
            va_point3 = QPointF(8.0, 17.0)
            self._arrow_vertical = (va_point1, va_point2, va_point3)
            # arrow
            self._arrow = None
            self.setArrow(int(collapsed))

        def setArrow(self, arrow_dir):
            if arrow_dir:
                self._arrow = self._arrow_vertical
            else:
                self._arrow = self._arrow_horizontal

        def paintEvent(self, event):
            painter = QPainter()
            painter.begin(self)
            painter.setBrush(QColor(192, 192, 192))
            painter.setPen(QColor(64, 64, 64))
            if self.env_desc.lib_qt == 'pyqt5':
                painter.drawPolygon(*self._arrow)
            else:  # 'pyside'
                painter.drawPolygon(self._arrow)
            painter.end()

idaclu/qt_widgets.py
harlamism/IdaClu
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'idacluaBebKo.ui'
##
## Created by: Qt User Interface Compiler version 5.15.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

# from PySide2.QtCore import *
# from PySide2.QtGui import *
# from PySide2.QtWidgets import *
from idaclu.qt_shims import (
    Signal,
    QSizePolicy,
    QAbstractItemView,
    QComboBox,
    QCoreApplication,
    QCursor,
    QEvent,
    QFont,
    QFrame,
    QGroupBox,
    QHBoxLayout,
    QIcon,
    QLineEdit,
    QMetaObject,
    QProgressBar,
    QPushButton,
    QRect,
    QScrollArea,
    QSize,
    QSizePolicy,
    QSpacerItem,
    QSplitter,
    QStandardItem,
    QStyledItemDelegate,
    Qt,
    QThread,
    QTreeView,
    QVBoxLayout,
    QWidget
)
from idaclu.qt_utils import i18n

from idaclu.qt_widgets import (
    FilterInputGroup,
    LabelTool,
    PaletteTool,
    ProgressIndicator
)

class Ui_PluginDialog(object):
    def __init__(self, env_desc):
        self.env_desc = env_desc

    def setupUi(self, PluginDialog):
        if not PluginDialog.objectName():
            PluginDialog.setObjectName(u"PluginDialog")
        PluginDialog.resize(1024, 600)
        icon = QIcon()
        icon.addFile(u":/idaclu/icon_64.png", QSize(), QIcon.Normal, QIcon.Off)
        PluginDialog.setWindowIcon(icon)
        self.vlPluginDialog = QVBoxLayout(PluginDialog)
        self.vlPluginDialog.setObjectName(u"vlPluginDialog")
        self.DialogSplitter = QSplitter(PluginDialog)
        self.DialogSplitter.setObjectName(u"DialogSplitter")
        self.DialogSplitter.setOrientation(Qt.Horizontal)
        self.DialogSplitter.setChildrenCollapsible(False)
        self.SidebarFrame = QFrame(self.DialogSplitter)
        self.SidebarFrame.setObjectName(u"SidebarFrame")
        self.SidebarLayout = QVBoxLayout(self.SidebarFrame)
        self.SidebarLayout.setSpacing(0)
        self.SidebarLayout.setObjectName(u"SidebarLayout")
        self.SidebarLayout.setContentsMargins(0, 0, 0, 0)
        self.ScriptsLayout = QVBoxLayout()
        self.ScriptsLayout.setSpacing(0)
        self.ScriptsLayout.setObjectName(u"ScriptsLayout")
        self.ScriptsHeader = QPushButton(self.SidebarFrame)
        self.ScriptsHeader.setObjectName(u"ScriptsHeader")
        self.ScriptsHeader.setMinimumSize(QSize(200, 30))
        font = QFont()
        font.setBold(True)
        font.setWeight(75)
        self.ScriptsHeader.setFont(font)
        self.ScriptsHeader.setCursor(QCursor(Qt.PointingHandCursor))
        self.ScriptsHeader.setProperty("class", "head")
        self.ScriptsLayout.addWidget(self.ScriptsHeader)

        self.ScriptsArea = QScrollArea(self.SidebarFrame)
        self.ScriptsArea.setObjectName(u"ScriptsArea")
        self.ScriptsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.ScriptsArea.setWidgetResizable(True)
        self.wScriptsContents = QWidget()
        self.wScriptsContents.setObjectName(u"wScriptsContents")
        self.wScriptsContents.setGeometry(QRect(0, 0, 235, 372))

        # custom layout
        self.ScriptsContentsLayout = QVBoxLayout(self.wScriptsContents)
        self.ScriptsContentsLayout.setSpacing(0)
        self.ScriptsContentsLayout.setAlignment(Qt.AlignTop)

        self.ScriptsArea.setWidget(self.wScriptsContents)

        self.ScriptsLayout.addWidget(self.ScriptsArea)


        self.SidebarLayout.addLayout(self.ScriptsLayout)

        self.sScriptsBottom = QSpacerItem(20, 10, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.SidebarLayout.addItem(self.sScriptsBottom)

        self.FiltersLayout = QVBoxLayout()
        self.FiltersLayout.setSpacing(0)
        self.FiltersLayout.setObjectName(u"FiltersLayout")
        self.FiltersHeader = QPushButton(self.SidebarFrame)
        self.FiltersHeader.setObjectName(u"FiltersHeader")
        self.FiltersHeader.setMinimumSize(QSize(200, 30))
        font1 = QFont()
        font1.setBold(True)
        font1.setWeight(75)
        self.FiltersHeader.setFont(font1)
        self.FiltersHeader.setCursor(QCursor(Qt.PointingHandCursor))
        self.FiltersHeader.setProperty("class", "head")
        self.FiltersLayout.addWidget(self.FiltersHeader)

        self.FiltersGroup = QGroupBox(self.SidebarFrame)
        self.FiltersGroup.setObjectName(u"FiltersGroup")
        self.vlFiltersGroup = QVBoxLayout(self.FiltersGroup)
        self.vlFiltersGroup.setSpacing(0)
        self.vlFiltersGroup.setObjectName(u"vlFiltersGroup")
        self.vlFiltersGroup.setContentsMargins(0, 0, 0, 0)
        self.sFilters1 = QSpacerItem(20, 15, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.vlFiltersGroup.addItem(self.sFilters1)

        self.FolderFilterLayout = QHBoxLayout()
        self.FolderFilterLayout.setObjectName(u"FolderFilterLayout")
        self.sFolderFilterBeg = QSpacerItem(20, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.FolderFilterLayout.addItem(self.sFolderFilterBeg)

        self.wFolderFilter = FilterInputGroup(u"Folders", u"Pick folders...", self.FiltersGroup)
        self.wFolderFilter.setObjectName(u"wFolderFilter")
        self.wFolderFilter.setMinimumSize(QSize(0, 26))
        self.wFolderFilter.setMaximumSize(QSize(16777215, 26))

        self.FolderFilterLayout.addWidget(self.wFolderFilter)

        self.sFolderFilterEnd = QSpacerItem(20, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.FolderFilterLayout.addItem(self.sFolderFilterEnd)


        self.vlFiltersGroup.addLayout(self.FolderFilterLayout)

        self.sFilters2 = QSpacerItem(20, 15, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.vlFiltersGroup.addItem(self.sFilters2)

        self.PrefixFilterLayout = QHBoxLayout()
        self.PrefixFilterLayout.setObjectName(u"PrefixFilterLayout")
        self.sPrefixFilterBeg = QSpacerItem(20, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.PrefixFilterLayout.addItem(self.sPrefixFilterBeg)

        self.wPrefixFilter = FilterInputGroup(u"Prefixes", u"Pick prefixes...", self.FiltersGroup)
        self.wPrefixFilter.setObjectName(u"wPrefixFilter")
        self.wPrefixFilter.setMinimumSize(QSize(0, 26))
        self.wPrefixFilter.setMaximumSize(QSize(16777215, 26))

        self.PrefixFilterLayout.addWidget(self.wPrefixFilter)

        self.sPrefixFilterEnd = QSpacerItem(20, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.PrefixFilterLayout.addItem(self.sPrefixFilterEnd)


        self.vlFiltersGroup.addLayout(self.PrefixFilterLayout)

        self.sFilters3 = QSpacerItem(20, 15, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.vlFiltersGroup.addItem(self.sFilters3)

        self.ColorFilterLayout = QHBoxLayout()
        self.ColorFilterLayout.setObjectName(u"ColorFilterLayout")
        self.sColorFilterBeg = QSpacerItem(40, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.ColorFilterLayout.addItem(self.sColorFilterBeg)

        self.wColorFilter = PaletteTool(
            u"ColorFilter",
            (26, 26),
            u"Filter",
            True,
            False,
            self.FiltersGroup)
        self.wColorFilter.setObjectName(u"wColorFilter")
        self.wColorFilter.setMinimumSize(QSize(0, 26))
        self.wColorFilter.setMaximumSize(QSize(16777215, 26))

        self.ColorFilterLayout.addWidget(self.wColorFilter)

        self.sColorFilterEnd = QSpacerItem(40, 26, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.ColorFilterLayout.addItem(self.sColorFilterEnd)


        self.vlFiltersGroup.addLayout(self.ColorFilterLayout)

        self.sFilters4 = QSpacerItem(20, 15, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.vlFiltersGroup.addItem(self.sFilters4)


        self.FiltersLayout.addWidget(self.FiltersGroup)


        self.SidebarLayout.addLayout(self.FiltersLayout)

        self.sFiltersBottom = QSpacerItem(20, 14, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.SidebarLayout.addItem(self.sFiltersBottom)

        self.DialogSplitter.addWidget(self.SidebarFrame)
        self.MainFrame = QFrame(self.DialogSplitter)
        self.MainFrame.setObjectName(u"MainFrame")
        self.MainLayout = QVBoxLayout(self.MainFrame)
        self.MainLayout.setSpacing(0)
        self.MainLayout.setObjectName(u"MainLayout")
        self.MainLayout.setContentsMargins(5, 0, 0, 0)
        self.wProgressBar = ProgressIndicator(self.MainFrame)
        self.wProgressBar.setObjectName(u"wProgressBar")
        self.wProgressBar.setMinimumSize(QSize(0, 5))
        self.wProgressBar.setMaximumSize(QSize(16777215, 5))

        self.MainLayout.addWidget(self.wProgressBar)

        self.wResultsView = QWidget(self.MainFrame)
        self.wResultsView.setObjectName(u"wResultsView")
        self.hlResultsView = QHBoxLayout(self.wResultsView)
        self.hlResultsView.setObjectName(u"hlResultsView")
        self.hlResultsView.setContentsMargins(0, 0, 0, 0)
        self.rvTable = QTreeView(self.wResultsView)
        self.rvTable.setObjectName(u"rvTable")
        self.rvTable.setContextMenuPolicy(Qt.CustomContextMenu)
        self.rvTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.rvTable.setAlternatingRowColors(True)
        self.rvTable.setSelectionMode(QAbstractItemView.ExtendedSelection)

        self.hlResultsView.addWidget(self.rvTable)


        self.MainLayout.addWidget(self.wResultsView)

        self.sToolsTop = QSpacerItem(20, 10, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.MainLayout.addItem(self.sToolsTop)

        self.ToolsLayout = QHBoxLayout()
        self.ToolsLayout.setObjectName(u"ToolsLayout")
        self.sToolsBeg = QSpacerItem(10, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.ToolsLayout.addItem(self.sToolsBeg)

        self.wLabelTool = LabelTool(u"LabelTool", self.env_desc, self.MainFrame)
        self.wLabelTool.setObjectName(u"wLabelTool")
        self.wLabelTool.setMinimumSize(QSize(320, 30))
        self.wLabelTool.setMaximumSize(QSize(16777215, 30))

        self.ToolsLayout.addWidget(self.wLabelTool)

        self.sToolsMid = QSpacerItem(80, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)

        self.ToolsLayout.addItem(self.sToolsMid)

        self.wColorTool = PaletteTool(
            u"PaletteTool",
            (30, 30),
            u"SetColor",
            False,
            True,
            self.MainFrame)
        self.wColorTool.setObjectName(u"wColorTool")
        self.wColorTool.setMinimumSize(QSize(192, 30))
        self.wColorTool.setMaximumSize(QSize(16777215, 30))

        self.ToolsLayout.addWidget(self.wColorTool)

        self.sToolsEnd = QSpacerItem(10, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)

        self.ToolsLayout.addItem(self.sToolsEnd)


        self.MainLayout.addLayout(self.ToolsLayout)

        self.sToolsBottom = QSpacerItem(20, 14, QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.MainLayout.addItem(self.sToolsBottom)

        self.MainLayout.setStretch(1, 8)
        self.MainLayout.setStretch(2, 1)
        self.MainLayout.setStretch(4, 1)
        self.DialogSplitter.addWidget(self.MainFrame)

        self.vlPluginDialog.addWidget(self.DialogSplitter)


        self.retranslateUi(PluginDialog)

        QMetaObject.connectSlotsByName(PluginDialog)
    # setupUi

    def retranslateUi(self, PluginDialog):
        PluginDialog.setWindowTitle(i18n("IdaClu v0.9"))
        self.ScriptsHeader.setText(i18n("TOOLSET"))
        self.FiltersHeader.setText(i18n("FILTERS"))
    # retranslateUi

idaclu/ui_idaclu.py
harlamism/IdaClu