cheerleader 0.2.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.
- cheerleader/.DS_Store +0 -0
- cheerleader/__init__.py +44 -0
- cheerleader/agent/__init__.py +9 -0
- cheerleader/agent/agent.py +81 -0
- cheerleader/agent/agent_settings.py +46 -0
- cheerleader/formats/__init__.py +45 -0
- cheerleader/formats/base.py +11 -0
- cheerleader/formats/elf.py +730 -0
- cheerleader/formats/macho.py +682 -0
- cheerleader/libs/__init__.py +0 -0
- cheerleader/libs/cfg.py +201 -0
- cheerleader/libs/disasm.py +119 -0
- cheerleader/libs/types.py +165 -0
- cheerleader/tui/__init__.py +3 -0
- cheerleader/tui/app.py +156 -0
- cheerleader/tui/app.tcss +191 -0
- cheerleader/tui/highlight.py +208 -0
- cheerleader/tui/screens.py +175 -0
- cheerleader/tui/tabs.py +861 -0
- cheerleader/tui/widgets.py +111 -0
- cheerleader/util/__init__.py +14 -0
- cheerleader/util/json_parse.py +32 -0
- cheerleader/util/utils.py +27 -0
- cheerleader/util/uuids.py +23 -0
- cheerleader-0.2.0.dist-info/METADATA +643 -0
- cheerleader-0.2.0.dist-info/RECORD +28 -0
- cheerleader-0.2.0.dist-info/WHEEL +4 -0
- cheerleader-0.2.0.dist-info/entry_points.txt +3 -0
cheerleader/.DS_Store
ADDED
|
Binary file
|
cheerleader/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""disasm — binary inspector (Mach-O, ELF)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from cheerleader.tui.app import APP_TITLE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog=APP_TITLE,
|
|
12
|
+
description="Binary inspector TUI",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("binary", help="path to the binary to inspect")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--hex",
|
|
17
|
+
action="store_true",
|
|
18
|
+
default=False,
|
|
19
|
+
help="open in hex editor mode (raw file hex dump only)",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--env",
|
|
23
|
+
default=".env",
|
|
24
|
+
metavar="FILE",
|
|
25
|
+
help="path to .env file for AI agent config (default: .env)",
|
|
26
|
+
)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
open(args.binary, "rb").close()
|
|
31
|
+
except OSError as e:
|
|
32
|
+
print(f"Error: {e}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
from cheerleader.formats import detect_format
|
|
36
|
+
|
|
37
|
+
fmt = detect_format(args.binary)
|
|
38
|
+
if fmt == "unknown":
|
|
39
|
+
print("Warning: unrecognised binary format — continuing anyway")
|
|
40
|
+
|
|
41
|
+
from cheerleader.tui import DisasmApp
|
|
42
|
+
|
|
43
|
+
app = DisasmApp(args.binary, env_file=args.env, hex_mode=args.hex)
|
|
44
|
+
app.run()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# disassembly agent
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from deepagents import create_deep_agent
|
|
6
|
+
from langchain.chat_models import init_chat_model, BaseChatModel
|
|
7
|
+
from langchain_core.messages import HumanMessage
|
|
8
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
9
|
+
|
|
10
|
+
# the agent class
|
|
11
|
+
class CheerleaderAIAgent(object):
|
|
12
|
+
def __init__(self, config: dict[str, Any]):
|
|
13
|
+
self.provider: str = config.get("provider")
|
|
14
|
+
self.llm_model: str = config.get("llm_model")
|
|
15
|
+
self.base_url: str = config.get("base_url")
|
|
16
|
+
self.api_key: str = config.get("api_key")
|
|
17
|
+
self.system_prompt: str = config.get("system_prompt")
|
|
18
|
+
self.skills_path: Path = Path(config.get("skills_path"))
|
|
19
|
+
|
|
20
|
+
# at the beginning, connection is empty
|
|
21
|
+
self.llm_connector: BaseChatModel = None
|
|
22
|
+
self.agent: CompiledStateGraph = None
|
|
23
|
+
try:
|
|
24
|
+
self.llm_connector: BaseChatModel = self._create_model_connector()
|
|
25
|
+
self.agent: CompiledStateGraph = self._create_agent()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
raise e from e
|
|
28
|
+
|
|
29
|
+
# create a connection to a custom supported endpoint
|
|
30
|
+
# specify model name and apikey
|
|
31
|
+
def _create_model_connector(self) -> BaseChatModel:
|
|
32
|
+
try:
|
|
33
|
+
return init_chat_model(
|
|
34
|
+
model=self.llm_model,
|
|
35
|
+
model_provider=self.provider,
|
|
36
|
+
api_key=self.api_key if self.api_key != "" else "sk-unprotected",
|
|
37
|
+
base_url=self.base_url,
|
|
38
|
+
)
|
|
39
|
+
except Exception:
|
|
40
|
+
import logging
|
|
41
|
+
|
|
42
|
+
msg = f"Failed to initialize chat model: provider={self.provider}, base_url={self.base_url}, model={self.llm_model}"
|
|
43
|
+
|
|
44
|
+
logging.critical(msg)
|
|
45
|
+
raise Exception(msg)
|
|
46
|
+
|
|
47
|
+
# create reverser agent with langchain deepagent library
|
|
48
|
+
def _create_agent(self) -> CompiledStateGraph:
|
|
49
|
+
try:
|
|
50
|
+
# resolve skills path
|
|
51
|
+
if not self.skills_path.exists():
|
|
52
|
+
raise Exception(f"Skills dir {self.skills_path.absolute()} not found")
|
|
53
|
+
|
|
54
|
+
# check connector
|
|
55
|
+
if self.llm_connector is None:
|
|
56
|
+
raise Exception(f"Connector for model {self.llm_model}, using URL {self.base_url} is not available.")
|
|
57
|
+
|
|
58
|
+
# create a filesystem backend to load skills from
|
|
59
|
+
from deepagents.backends.filesystem import FilesystemBackend
|
|
60
|
+
fs_backend = FilesystemBackend(root_dir=self.skills_path.absolute().parent)
|
|
61
|
+
|
|
62
|
+
# create the agent object
|
|
63
|
+
return create_deep_agent(
|
|
64
|
+
model=self.llm_connector,
|
|
65
|
+
backend=fs_backend,
|
|
66
|
+
system_prompt=self.system_prompt,
|
|
67
|
+
skills=[self.skills_path.name]
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
import logging
|
|
71
|
+
msg = f"Failed to initialize Agent: model={self.llm_model}"
|
|
72
|
+
|
|
73
|
+
logging.critical(msg)
|
|
74
|
+
raise Exception(msg)
|
|
75
|
+
|
|
76
|
+
# main method
|
|
77
|
+
def invoke(self, user_prompt: str):
|
|
78
|
+
payload = {
|
|
79
|
+
"messages": [HumanMessage(content=user_prompt)]
|
|
80
|
+
}
|
|
81
|
+
return self.agent.invoke(payload)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Resolved agent configuration: ``.env`` overrides defaults from ``src.config``.
|
|
4
|
+
|
|
5
|
+
Load order: ``python-dotenv`` reads the project ``.env`` into ``os.environ``;
|
|
6
|
+
each setting prefers a non-empty environment value, otherwise the value from
|
|
7
|
+
``src.config``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# import required modules
|
|
15
|
+
try:
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
from cheerleader.util.utils import _str_setting
|
|
18
|
+
except ImportError:
|
|
19
|
+
raise ImportError("python-dotenv is required to use this module")
|
|
20
|
+
|
|
21
|
+
# load the environment variables
|
|
22
|
+
def load_environment_variables(envfile: str) -> dict[str, Any]:
|
|
23
|
+
e_file = Path(envfile)
|
|
24
|
+
if not e_file.exists():
|
|
25
|
+
raise FileNotFoundError(f"Environment file {e_file} not found")
|
|
26
|
+
load_dotenv(e_file) # load the environment variables
|
|
27
|
+
return _get_settings_from_environment()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# get the settings
|
|
31
|
+
def _get_settings_from_environment():
|
|
32
|
+
settings = {
|
|
33
|
+
"provider": _str_setting("LLM_PROVIDER", "openai"),
|
|
34
|
+
"llm_model": _str_setting("LLM_MODEL", "gpt-4o-mini"),
|
|
35
|
+
"base_url": _str_setting("BASE_URL", "https://api.openai.com/v1"),
|
|
36
|
+
"api_key": _str_setting("API_KEY", "your-api-key-here"),
|
|
37
|
+
"skills_path": _str_setting("SKILLS_PATH", "./skills"),
|
|
38
|
+
"system_prompt": _str_setting("SYSTEM_PROMPT", "you are an expert in reverse engineering"),
|
|
39
|
+
}
|
|
40
|
+
return settings
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# export the settings
|
|
44
|
+
__all__ = [
|
|
45
|
+
"load_environment_variables",
|
|
46
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Binary format detection and parser dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
from cheerleader.libs.types import BinaryInfo
|
|
8
|
+
|
|
9
|
+
_MACHO_MAGICS = frozenset({
|
|
10
|
+
0xFEEDFACE, 0xCEFAEDFE, # 32-bit LE/BE
|
|
11
|
+
0xFEEDFACF, 0xCFFAEDFE, # 64-bit LE/BE
|
|
12
|
+
0xCAFEBABE, 0xBEBAFECA, # fat LE/BE
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_format(path: str) -> str:
|
|
17
|
+
"""Return "macho", "elf", or "unknown" by reading the first 4 bytes."""
|
|
18
|
+
try:
|
|
19
|
+
with open(path, "rb") as f:
|
|
20
|
+
raw = f.read(4)
|
|
21
|
+
except OSError:
|
|
22
|
+
return "unknown"
|
|
23
|
+
if len(raw) < 4:
|
|
24
|
+
return "unknown"
|
|
25
|
+
if raw[:4] == b'\x7fELF':
|
|
26
|
+
return "elf"
|
|
27
|
+
le_val = struct.unpack_from("<I", raw)[0]
|
|
28
|
+
if le_val in _MACHO_MAGICS:
|
|
29
|
+
return "macho"
|
|
30
|
+
be_val = struct.unpack_from(">I", raw)[0]
|
|
31
|
+
if be_val in _MACHO_MAGICS:
|
|
32
|
+
return "macho"
|
|
33
|
+
return "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse(path: str, **kwargs) -> BinaryInfo:
|
|
37
|
+
"""Detect format and dispatch to the appropriate parser."""
|
|
38
|
+
fmt = detect_format(path)
|
|
39
|
+
if fmt == "macho":
|
|
40
|
+
from cheerleader.formats.macho import parse as _macho_parse
|
|
41
|
+
return _macho_parse(path, **kwargs)
|
|
42
|
+
if fmt == "elf":
|
|
43
|
+
from cheerleader.formats.elf import parse as _elf_parse
|
|
44
|
+
return _elf_parse(path, **kwargs)
|
|
45
|
+
return BinaryInfo(path=path, error="Unrecognised file format")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Abstract parser protocol for binary formats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from cheerleader.libs.types import BinaryInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FormatParser(Protocol):
|
|
11
|
+
def parse(self, path: str, **kwargs) -> BinaryInfo: ...
|