declib 3.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. declib/__init__.py +9 -0
  2. declib/__main__.py +190 -0
  3. declib/api/__init__.py +13 -0
  4. declib/api/artifact_dict.py +153 -0
  5. declib/api/artifact_lifter.py +161 -0
  6. declib/api/decompiler_client.py +1219 -0
  7. declib/api/decompiler_interface.py +1261 -0
  8. declib/api/decompiler_server.py +782 -0
  9. declib/api/server_registry.py +171 -0
  10. declib/api/type_definition_parser.py +201 -0
  11. declib/api/type_parser.py +409 -0
  12. declib/api/utils.py +31 -0
  13. declib/artifacts/__init__.py +93 -0
  14. declib/artifacts/artifact.py +311 -0
  15. declib/artifacts/comment.py +49 -0
  16. declib/artifacts/context.py +61 -0
  17. declib/artifacts/decompilation.py +35 -0
  18. declib/artifacts/enum.py +53 -0
  19. declib/artifacts/formatting.py +27 -0
  20. declib/artifacts/func.py +433 -0
  21. declib/artifacts/global_variable.py +31 -0
  22. declib/artifacts/patch.py +49 -0
  23. declib/artifacts/segment.py +37 -0
  24. declib/artifacts/stack_variable.py +50 -0
  25. declib/artifacts/struct.py +184 -0
  26. declib/artifacts/typedef.py +59 -0
  27. declib/cli/__init__.py +3 -0
  28. declib/cli/decompiler_cli.py +1487 -0
  29. declib/configuration.py +184 -0
  30. declib/decompiler_stubs/__init__.py +0 -0
  31. declib/decompiler_stubs/angr_declib/__init__.py +4 -0
  32. declib/decompiler_stubs/binja_declib/__init__.py +4 -0
  33. declib/decompiler_stubs/ida_declib.py +8 -0
  34. declib/decompilers/__init__.py +8 -0
  35. declib/decompilers/angr/__init__.py +11 -0
  36. declib/decompilers/angr/artifact_lifter.py +46 -0
  37. declib/decompilers/angr/compat.py +262 -0
  38. declib/decompilers/angr/interface.py +949 -0
  39. declib/decompilers/binja/__init__.py +0 -0
  40. declib/decompilers/binja/artifact_lifter.py +32 -0
  41. declib/decompilers/binja/hooks.py +201 -0
  42. declib/decompilers/binja/interface.py +795 -0
  43. declib/decompilers/ghidra/__init__.py +0 -0
  44. declib/decompilers/ghidra/artifact_lifter.py +60 -0
  45. declib/decompilers/ghidra/compat/__init__.py +0 -0
  46. declib/decompilers/ghidra/compat/headless.py +156 -0
  47. declib/decompilers/ghidra/compat/imports.py +78 -0
  48. declib/decompilers/ghidra/compat/state.py +54 -0
  49. declib/decompilers/ghidra/compat/transaction.py +30 -0
  50. declib/decompilers/ghidra/hooks.py +242 -0
  51. declib/decompilers/ghidra/interface.py +1433 -0
  52. declib/decompilers/ida/__init__.py +0 -0
  53. declib/decompilers/ida/artifact_lifter.py +51 -0
  54. declib/decompilers/ida/compat.py +2054 -0
  55. declib/decompilers/ida/hooks.py +700 -0
  56. declib/decompilers/ida/ida_ui.py +80 -0
  57. declib/decompilers/ida/interface.py +659 -0
  58. declib/logger.py +101 -0
  59. declib/plugin_installer.py +259 -0
  60. declib/skills/__init__.py +24 -0
  61. declib/skills/decompiler/SKILL.md +316 -0
  62. declib/ui/__init__.py +33 -0
  63. declib/ui/qt_objects.py +146 -0
  64. declib/ui/utils.py +115 -0
  65. declib/ui/version.py +14 -0
  66. declib-3.8.0.dist-info/METADATA +138 -0
  67. declib-3.8.0.dist-info/RECORD +71 -0
  68. declib-3.8.0.dist-info/WHEEL +5 -0
  69. declib-3.8.0.dist-info/entry_points.txt +3 -0
  70. declib-3.8.0.dist-info/licenses/LICENSE +24 -0
  71. declib-3.8.0.dist-info/top_level.txt +1 -0
declib/logger.py ADDED
@@ -0,0 +1,101 @@
1
+ import logging
2
+ import logging.config
3
+ import tempfile
4
+ from datetime import datetime
5
+
6
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
7
+ _, tempfilename = tempfile.mkstemp(prefix=timestamp + '.declib.', suffix='.log')
8
+ string_format = "%(levelname)s | %(asctime)s | %(name)-8s | %(message)s"
9
+
10
+
11
+ default_config = {
12
+ "version": 1,
13
+ "disable_existing_loggers": False,
14
+ "formatters": {
15
+ "console": {
16
+ "format": string_format
17
+ },
18
+ "logfile": {
19
+ "format": string_format
20
+ },
21
+ },
22
+
23
+ "handlers": {
24
+ "console": {
25
+ "class": "logging.StreamHandler",
26
+ "level": "DEBUG",
27
+ "formatter": "console",
28
+ "stream": "ext://sys.stdout"
29
+ },
30
+
31
+ "local_file_handler": {
32
+ "class": "logging.handlers.RotatingFileHandler",
33
+ "level": "DEBUG",
34
+ "formatter": "logfile",
35
+ "filename": tempfilename,
36
+ "maxBytes": 1000000,
37
+ "backupCount": 20,
38
+ "encoding": "utf8",
39
+ "delay": True
40
+ }
41
+ },
42
+ 'loggers': {
43
+ 'declib': {
44
+ 'handlers': ["console", "local_file_handler"],
45
+ 'level': 'INFO',
46
+ 'propagate': False
47
+ },
48
+ }
49
+ }
50
+
51
+
52
+ class Loggers:
53
+ """
54
+ Logger Manager.
55
+ """
56
+ IN_SCOPE_LOGGERS = ('declib', )
57
+
58
+ def __init__(self):
59
+ self._loggers = {}
60
+ self.load_all_loggers()
61
+ self.profiling_enabled = False
62
+
63
+ # disable filelock info logs
64
+ logging.getLogger("filelock").setLevel(logging.WARNING)
65
+
66
+ self.config_dict = None
67
+ if default_config is not None:
68
+ self.config_dict = default_config
69
+ if self.config_dict is not None:
70
+ logging.config.dictConfig(self.config_dict)
71
+ self.handler = logging.StreamHandler()
72
+ self.handler.setFormatter(logging.Formatter('%(levelname)-7s | %(asctime)-23s | %(name)-8s | %(message)s'))
73
+
74
+ def load_all_loggers(self):
75
+ for name, logger in logging.Logger.manager.loggerDict.items():
76
+ if any(name.startswith(x + '.') or name == x for x in self.IN_SCOPE_LOGGERS):
77
+ self._loggers[name] = logger
78
+
79
+ def __getattr__(self, k):
80
+ real_k = k.replace('_', '.')
81
+ if real_k in self._loggers:
82
+ return self._loggers[real_k]
83
+ else:
84
+ raise AttributeError(k)
85
+
86
+ def __dir__(self):
87
+ return list(super(Loggers, self).__dir__()) + list(self._loggers.keys())
88
+
89
+
90
+ def is_enabled_for(logger, level):
91
+ if level == 1:
92
+ from .. import loggers
93
+ return loggers.profiling_enabled
94
+ return originalIsEnabledFor(logger, level)
95
+
96
+
97
+ originalIsEnabledFor = logging.Logger.isEnabledFor
98
+
99
+ # Override isEnabledFor() for Logger class
100
+ logging.Logger.isEnabledFor = is_enabled_for
101
+
@@ -0,0 +1,259 @@
1
+ import os
2
+ import platform
3
+ from pathlib import Path
4
+ import textwrap
5
+ import sys
6
+ import shutil
7
+ from typing import Optional, Union, Tuple
8
+
9
+ from prompt_toolkit import prompt
10
+ from prompt_toolkit.completion.filesystem import PathCompleter
11
+
12
+
13
+ class Color:
14
+ """
15
+ Used to colorify terminal output.
16
+ Taken from: https://github.com/hugsy/gef/blob/dev/tests/utils.py
17
+ """
18
+ NORMAL = "\x1b[0m"
19
+ GRAY = "\x1b[1;38;5;240m"
20
+ LIGHT_GRAY = "\x1b[0;37m"
21
+ RED = "\x1b[31m"
22
+ GREEN = "\x1b[32m"
23
+ YELLOW = "\x1b[33m"
24
+ BLUE = "\x1b[34m"
25
+ PINK = "\x1b[35m"
26
+ CYAN = "\x1b[36m"
27
+ BOLD = "\x1b[1m"
28
+ UNDERLINE = "\x1b[4m"
29
+ UNDERLINE_OFF = "\x1b[24m"
30
+ HIGHLIGHT = "\x1b[3m"
31
+ HIGHLIGHT_OFF = "\x1b[23m"
32
+ BLINK = "\x1b[5m"
33
+ BLINK_OFF = "\x1b[25m"
34
+
35
+
36
+ class PluginInstaller:
37
+ DECOMPILERS = (
38
+ "ida",
39
+ "binja",
40
+ "ghidra",
41
+ "angr"
42
+ )
43
+
44
+ DEBUGGERS = (
45
+ "gdb",
46
+ )
47
+
48
+ def __init__(self, targets=None, target_install_paths=None):
49
+ self.targets = targets if targets is not None else self.DECOMPILERS+self.DEBUGGERS
50
+ self._home = Path(os.getenv("HOME") or "~/").expanduser().absolute()
51
+ self.target_install_paths = target_install_paths or {} #or self._populate_installs_from_config()
52
+ self._successful_installs = {}
53
+
54
+ @staticmethod
55
+ def find_pkg_files(pkg_name):
56
+ if sys.version_info >= (3, 9):
57
+ import importlib.resources
58
+ path = str(importlib.resources.files(pkg_name))
59
+ else:
60
+ import pkg_resources
61
+ path = pkg_resources.resource_filename(pkg_name, "")
62
+
63
+ return Path(path).absolute()
64
+
65
+ def install(self, interactive=True, paths_by_target=None):
66
+ self.target_install_paths.update(paths_by_target or {})
67
+ self.display_prologue()
68
+
69
+ if interactive:
70
+ self.display_install_instructions()
71
+
72
+ try:
73
+ self.install_targets(interactive=interactive)
74
+ except Exception as e:
75
+ print(f"Stopping Install... because: {e}")
76
+ except KeyboardInterrupt:
77
+ print("Goodbye...")
78
+
79
+ self.display_epilogue()
80
+
81
+ def display_prologue(self):
82
+ pass
83
+
84
+ def display_install_instructions(self):
85
+ print(textwrap.dedent("""
86
+ Each decompiler/debugger will be prompted for install below. If you would like to skip install for something
87
+ you can enter 'n' or just hit enter. Each path prompt has tab path completion.
88
+ Enter nothing in each path prompt to get the default listed.
89
+ """))
90
+
91
+ def display_epilogue(self):
92
+ self.good("Plugin install completed! If anything was skipped by mistake, please manually install it.")
93
+
94
+ @staticmethod
95
+ def info(msg):
96
+ print(f"{Color.BLUE}{msg}{Color.NORMAL}")
97
+
98
+ @staticmethod
99
+ def good(msg):
100
+ print(f"{Color.GREEN}[+] {msg}{Color.NORMAL}")
101
+
102
+ @staticmethod
103
+ def warn(msg):
104
+ print(f"{Color.YELLOW}[!] {msg}{Color.NORMAL}")
105
+
106
+ @staticmethod
107
+ def ask_path(target, location, default=None) -> Optional[Union[bool, Path]]:
108
+ """
109
+ Possible return values:
110
+ - None: install failed or skipped
111
+ - Path: install succeeded
112
+ """
113
+
114
+ PluginInstaller.info(f"Install for {target}? [y/n]")
115
+ res = prompt("")
116
+ if res.lower() != "y":
117
+ return None
118
+
119
+ PluginInstaller.info(location + f" [default = {default}] (enter nothing to use default): ")
120
+ filepath = prompt("", completer=PathCompleter(expanduser=True))
121
+ if not filepath and default:
122
+ return default
123
+
124
+ filepath = Path(filepath).expanduser().absolute()
125
+ if not filepath.exists():
126
+ PluginInstaller.warn(f"Provided filepath {filepath} does not exist. {'Using default.' if default else 'Skipping.'}")
127
+ return default if default else None
128
+
129
+ return filepath
130
+
131
+ @staticmethod
132
+ def link_or_copy(src, dst, is_dir=False, symlink=False):
133
+ if platform.platform().startswith("Windows"):
134
+ # you can't symlink on windows, so just copy
135
+ symlink = False
136
+
137
+ # clean the install location
138
+ shutil.rmtree(dst, ignore_errors=True)
139
+ try:
140
+ os.unlink(dst)
141
+ except:
142
+ pass
143
+
144
+ if not symlink:
145
+ # copy if symlinking is not available on target system
146
+ if is_dir:
147
+ shutil.copytree(src, dst)
148
+ else:
149
+ shutil.copy(src, dst)
150
+ else:
151
+ # first attempt a symlink, if it works, exit early
152
+ try:
153
+ os.symlink(src, dst, target_is_directory=is_dir)
154
+ return
155
+ except:
156
+ pass
157
+
158
+ @staticmethod
159
+ def _get_path_without_ask(path, default_path=None, interactive=True) -> Tuple[Path, bool]:
160
+ path = Path(path) if path else None
161
+ if not interactive and path.exists():
162
+ return path, True
163
+
164
+ if path and path.exists():
165
+ default_path = path
166
+ else:
167
+ default_path = Path(default_path) if default_path else None
168
+ if not default_path or not default_path.exists():
169
+ default_path = None
170
+
171
+ return default_path, (not interactive and default_path and default_path.exists())
172
+
173
+ def install_targets(self, interactive=True):
174
+ for target in self.targets:
175
+ try:
176
+ target_installer = getattr(self, f"install_{target}")
177
+ except AttributeError:
178
+ continue
179
+
180
+ path = self.target_install_paths.get(f"{target}", None)
181
+ if path:
182
+ path = Path(path).expanduser().absolute()
183
+
184
+ if not path and not interactive:
185
+ continue
186
+
187
+ res = target_installer(path=path, interactive=interactive)
188
+ if res is None:
189
+ self.warn(f"Skipping or failed install for {target}... {Color.NORMAL}\n")
190
+ else:
191
+ self.good(f"Installed {target} to {res}\n")
192
+ self._successful_installs[target] = res
193
+ #GlobalConfig.update_or_make(self._home, **{f"{target}_path": res.parent})
194
+
195
+ def install_ida(self, path=None, interactive=True):
196
+ default_path, skip_ask = self._get_path_without_ask(
197
+ path, default_path=self._home.joinpath(".idapro").joinpath("plugins").expanduser(), interactive=interactive
198
+ )
199
+ return self.ask_path("IDA Pro", "Plugins Path", default=default_path) if not skip_ask \
200
+ else default_path
201
+
202
+ def install_ghidra(self, path=None, interactive=True):
203
+ potential_path = self._home.joinpath('ghidra_scripts').expanduser()
204
+ if self._home.exists() and not potential_path.exists():
205
+ self.info(f"Creating Ghidra Scripts directory at {potential_path}...")
206
+ potential_path.mkdir()
207
+
208
+ default_path, skip_ask = self._get_path_without_ask(
209
+ path, default_path=potential_path, interactive=interactive
210
+ )
211
+ return self.ask_path("Ghidra", "Ghidra Scripts Path", default=default_path) if not skip_ask \
212
+ else default_path
213
+
214
+ def install_binja(self, path=None, interactive=True):
215
+ os_name = platform.system()
216
+ if os_name == "Windows":
217
+ default_path = Path(os.environ.get("APPDATA", str(self._home))) / "Binary Ninja" / "plugins"
218
+ elif os_name == "Darwin":
219
+ default_path = (self._home / "Library" / "Application Support" / "Binary Ninja" / "plugins").expanduser()
220
+ else:
221
+ default_path = (self._home / ".binaryninja" / "plugins").expanduser()
222
+ default_path, skip_ask = self._get_path_without_ask(
223
+ path, default_path=default_path,
224
+ interactive=interactive
225
+ )
226
+ return self.ask_path("Binary Ninja", "Plugins Path", default=default_path) if not skip_ask \
227
+ else default_path
228
+
229
+ def install_angr(self, path=None, interactive=True):
230
+ # attempt to find the plugins folder for angr-management which is installed via pip
231
+ angr_resolved = True
232
+ try:
233
+ import angrmanagement
234
+ except ImportError:
235
+ angr_resolved = False
236
+ default_path = Path(angrmanagement.__file__).parent / "plugins" if angr_resolved else None
237
+
238
+ default_path, skip_ask = self._get_path_without_ask(path, default_path=default_path, interactive=interactive)
239
+ return self.ask_path("angr-management", "angr-management Plugins Path", default=default_path) if not skip_ask \
240
+ else default_path
241
+
242
+ def install_gdb(self, path=None, interactive=True):
243
+ default_path, skip_ask = self._get_path_without_ask(
244
+ path, default_path=self._home.joinpath(".gdbinit").expanduser(),
245
+ interactive=interactive
246
+ )
247
+ return self.ask_path("GDB", "gdbinit Path", default=default_path) if not skip_ask \
248
+ else default_path
249
+
250
+
251
+ class DecLibPluginInstaller(PluginInstaller):
252
+ def __init__(self, targets=None, target_install_paths=None):
253
+ targets = targets or PluginInstaller.DECOMPILERS
254
+ super().__init__(targets=targets, target_install_paths=target_install_paths)
255
+ self._declib_plugins_path = self.find_pkg_files("declib").joinpath("decompiler_stubs")
256
+
257
+ def display_prologue(self):
258
+ print(textwrap.dedent("""
259
+ Now installing DecLib plugins for all supported decompilers..."""))
@@ -0,0 +1,24 @@
1
+ """Bundled Agent Skills for declib.
2
+
3
+ Each subdirectory holds a SKILL.md (and any optional resources) that an LLM can
4
+ load to learn how to drive declib via the `decompiler` CLI. Use
5
+ `decompiler install-skill` to copy a skill into Claude Code or Codex.
6
+ """
7
+ from pathlib import Path
8
+
9
+ SKILLS_DIR = Path(__file__).parent
10
+
11
+
12
+ def available_skills() -> list[str]:
13
+ return sorted(
14
+ p.name
15
+ for p in SKILLS_DIR.iterdir()
16
+ if p.is_dir() and (p / "SKILL.md").is_file()
17
+ )
18
+
19
+
20
+ def skill_path(name: str) -> Path:
21
+ path = SKILLS_DIR / name
22
+ if not (path / "SKILL.md").is_file():
23
+ raise FileNotFoundError(f"Unknown bundled skill: {name!r}")
24
+ return path