Skip to content

Commit b4aed9d

Browse files
authored
Merge pull request #113 from Neverbolt/logging_documentation
New Argument Parsing
2 parents 0cf164b + 98d3204 commit b4aed9d

File tree

16 files changed

+768
-309
lines changed

16 files changed

+768
-309
lines changed

‎README.md‎

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,54 @@ $ vi .env
177177

178178
# if you start wintermute without parameters, it will list all available use cases
179179
$ python src/hackingBuddyGPT/cli/wintermute.py
180-
usage: wintermute.py [-h]
181-
{LinuxPrivesc,WindowsPrivesc,ExPrivEscLinux,ExPrivEscLinuxTemplated,ExPrivEscLinuxHintFile,ExPrivEscLinuxLSE,MinimalWebTesting,WebTestingWithExplanation,SimpleWebAPITesting,SimpleWebAPIDocumentation}
182-
...
183-
wintermute.py: error: the following arguments are required: {LinuxPrivesc,WindowsPrivesc,ExPrivEscLinux,ExPrivEscLinuxTemplated,ExPrivEscLinuxHintFile,ExPrivEscLinuxLSE,MinimalWebTesting,WebTestingWithExplanation,SimpleWebAPITesting,SimpleWebAPIDocumentation}
180+
No command provided
181+
usage: src/hackingBuddyGPT/cli/wintermute.py <command> [--help] [--config config.json] [options...]
182+
183+
commands:
184+
ExPrivEscLinux Showcase Minimal Linux Priv-Escalation
185+
ExPrivEscLinuxTemplated Showcase Minimal Linux Priv-Escalation
186+
LinuxPrivesc Linux Privilege Escalation
187+
WindowsPrivesc Windows Privilege Escalation
188+
ExPrivEscLinuxHintFile Linux Privilege Escalation using hints from a hint file initial guidance
189+
ExPrivEscLinuxLSE Linux Privilege Escalation using lse.sh for initial guidance
190+
WebTestingWithExplanation Minimal implementation of a web testing use case while allowing the llm to 'talk'
191+
SimpleWebAPIDocumentation Minimal implementation of a web API testing use case
192+
SimpleWebAPITesting Minimal implementation of a web API testing use case
193+
Viewer Webserver for (live) log viewing
194+
Replayer Tool to replay the .jsonl logs generated by the Viewer (not well tested)
195+
ThesisLinuxPrivescPrototype Thesis Linux Privilege Escalation Prototype
196+
197+
# to get more information about how to configure a use case you can call it with --help
198+
$ python src/hackingBuddyGPT/cli/wintermute.py LinuxPrivesc --help
199+
usage: src/hackingBuddyGPT/cli/wintermute.py LinuxPrivesc [--help] [--config config.json] [options...]
200+
201+
--log.log_server_address='localhost:4444' address:port of the log server to be used (default from builtin)
202+
--log.tag='' Tag for your current run (default from builtin)
203+
--log='local_logger' choice of logging backend (default from builtin)
204+
--log_db.connection_string='wintermute.sqlite3' sqlite3 database connection string for logs (default from builtin)
205+
--max_turns='30' (default from .env file, alternatives: 10 from builtin)
206+
--llm.api_key=<secret> OpenAI API Key (default from .env file)
207+
--llm.model OpenAI model name
208+
--llm.context_size='100000' Maximum context size for the model, only used internally for things like trimming to the context size (default from .env file)
209+
--llm.api_url='https://api.openai.com' URL of the OpenAI API (default from builtin)
210+
--llm.api_path='/v1/chat/completions' Path to the OpenAI API (default from builtin)
211+
--llm.api_timeout=240 Timeout for the API request (default from builtin)
212+
--llm.api_backoff=60 Backoff time in seconds when running into rate-limits (default from builtin)
213+
--llm.api_retries=3 Number of retries when running into rate-limits (default from builtin)
214+
--system='linux' (default from builtin)
215+
--enable_explanation=False (default from builtin)
216+
--enable_update_state=False (default from builtin)
217+
--disable_history=False (default from builtin)
218+
--hint='' (default from builtin)
219+
--conn.host
220+
--conn.hostname
221+
--conn.username
222+
--conn.password
223+
--conn.keyfilename
224+
--conn.port='2222' (default from .env file, alternatives: 22 from builtin)
184225
```
185226
186-
## Provide a Target Machine over SSH
227+
### Provide a Target Machine over SSH
187228
188229
The next important part is having a machine that we can run our agent against. In our case, the target machine will be situated at `192.168.122.151`.
189230
@@ -193,6 +234,23 @@ We are using vulnerable Linux systems running in Virtual Machines for this. Neve
193234
>
194235
> We are using virtual machines from our [Linux Privilege-Escalation Benchmark](https://github.com/ipa-lab/benchmark-privesc-linux) project. Feel free to use them for your own research!
195236
237+
## Using the web based viewer and replayer
238+
239+
If you want to have a better representation of the agent's output, you can use the web-based viewer. You can start it using `wintermute Viewer`, which will run the server on `http://127.0.0.1:4444` for the default `wintermute.sqlite3` database. You can change these options using the `--log_server_address` and `--log_db.connection_string` parameters.
240+
241+
Navigating to the log server address will show you an overview of all runs and clicking on a run will show you the details of that run. The viewer updates live using a websocket connection, and if you enable `Follow new runs` it will automatically switch to the new run when one is started.
242+
243+
Keep in mind that there is no additional protection for this webserver, other than how it can be reached (per default binding to `127.0.0.1` means it can only be reached from your local machine). If you make it accessible to the internet, everybody will be able to see all of your runs and also be able to inject arbitrary data into the database.
244+
245+
Therefore **DO NOT** make it accessible to the internet if you're not super sure about what you're doing!
246+
247+
There is also the experimental replay functionality, which can replay a run live from a capture file, including timing information. This is great for showcases and presentations, because it looks like everything is happening live and for real, but you know exactly what the results will be.
248+
249+
To use this, the run needs to be captured by a Viewer server by setting `--save_playback_dir` to a directory where the viewer can write the capture files.
250+
251+
With the Viewer server still running, you can then start `wintermute Replayer --replay_file <path_to_capture_file>` to replay the captured run (this will create a new run in the database).
252+
You can configure it to `--pause_on_message` and `--pause_on_tool_calls`, which will interrupt the replay at the respective points until enter is pressed in the shell where you run the Replayer in. You can also configure the `--playback_speed` to control the speed of the replay.
253+
196254
## Use Cases
197255
198256
GitHub Codespaces:

‎pyproject.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ dependencies = [
4545
'uvicorn[standard] == 0.30.6',
4646
'dataclasses_json == 0.6.7',
4747
'websockets == 13.1',
48+
'langchain-community',
49+
'langchain-openai',
50+
'markdown',
51+
'chromadb',
52+
'langchain-chroma',
4853
]
4954

5055
[project.urls]

‎src/hackingBuddyGPT/cli/wintermute.py‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22
import sys
33

44
from hackingBuddyGPT.usecases.base import use_cases
5+
from hackingBuddyGPT.utils.configurable import CommandMap, InvalidCommand, Parseable, instantiate
56

67

78
def main():
8-
parser = argparse.ArgumentParser()
9-
subparser = parser.add_subparsers(required=True)
10-
for name, use_case in use_cases.items():
11-
use_case.build_parser(subparser.add_parser(name=name, help=use_case.description))
12-
13-
parsed = parser.parse_args(sys.argv[1:])
14-
configuration = {k: v for k, v in vars(parsed).items() if k not in ("use_case", "parser_state")}
15-
instance = parsed.use_case(parsed)
16-
instance.init(configuration=configuration)
17-
instance.run()
9+
use_case_parsers: CommandMap = {
10+
name: Parseable(use_case, description=use_case.description)
11+
for name, use_case in use_cases.items()
12+
}
13+
try:
14+
instance, configuration = instantiate(sys.argv, use_case_parsers)
15+
except InvalidCommand as e:
16+
if len(f"{e}") > 0:
17+
print(e)
18+
print(e.usage)
19+
sys.exit(1)
20+
instance.run(configuration)
1821

1922

2023
if __name__ == "__main__":

‎src/hackingBuddyGPT/usecases/agents.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mako.template import Template
55
from typing import Dict
66

7-
from hackingBuddyGPT.utils.logging import log_conversation, GlobalLogger
7+
from hackingBuddyGPT.utils.logging import log_conversation, Logger, log_param
88
from hackingBuddyGPT.capabilities.capability import (
99
Capability,
1010
capabilities_to_simple_text_handler,
@@ -15,7 +15,7 @@
1515

1616
@dataclass
1717
class Agent(ABC):
18-
log: GlobalLogger = None
18+
log: Logger = log_param
1919

2020
_capabilities: Dict[str, Capability] = field(default_factory=dict)
2121
_default_capability: Capability = None

‎src/hackingBuddyGPT/usecases/base.py‎

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
import argparse
44
from dataclasses import dataclass
55

6-
from hackingBuddyGPT.utils.logging import GlobalLogger
6+
from hackingBuddyGPT.utils.logging import Logger, log_param
77
from typing import Dict, Type, TypeVar, Generic
88

9-
from hackingBuddyGPT.utils.configurable import ParameterDefinitions, build_parser, get_arguments, get_class_parameters, \
10-
Transparent, ParserState
11-
9+
from hackingBuddyGPT.utils.configurable import Transparent, configurable
1210

1311
@dataclass
1412
class UseCase(abc.ABC):
@@ -22,22 +20,21 @@ class UseCase(abc.ABC):
2220
so that they can be automatically discovered and run from the command line.
2321
"""
2422

25-
log: GlobalLogger
23+
log: Logger = log_param
2624

27-
def init(self, configuration):
25+
def init(self):
2826
"""
2927
The init method is called before the run method. It is used to initialize the UseCase, and can be used to
3028
perform any dynamic setup that is needed before the run method is called. One of the most common use cases is
3129
setting up the llm capabilities from the tools that were injected.
3230
"""
33-
self.configuration = configuration
34-
self.log.start_run(self.get_name(), self.serialize_configuration(configuration))
31+
pass
3532

3633
def serialize_configuration(self, configuration) -> str:
3734
return json.dumps(configuration)
3835

3936
@abc.abstractmethod
40-
def run(self):
37+
def run(self, configuration):
4138
"""
4239
The run method is the main method of the UseCase. It is used to run the UseCase, and should contain the main
4340
logic. It is recommended to have only the main llm loop in here, and call out to other methods for the
@@ -70,7 +67,10 @@ def before_run(self):
7067
def after_run(self):
7168
pass
7269

73-
def run(self):
70+
def run(self, configuration):
71+
self.configuration = configuration
72+
self.log.start_run(self.get_name(), self.serialize_configuration(configuration))
73+
7474
self.before_run()
7575

7676
turn = 1
@@ -98,31 +98,10 @@ def run(self):
9898
raise
9999

100100

101-
@dataclass
102-
class _WrappedUseCase:
103-
"""
104-
A WrappedUseCase should not be used directly and is an internal tool used for initialization and dependency injection
105-
of the actual UseCases.
106-
"""
107-
108-
name: str
109-
description: str
110-
use_case: Type[UseCase]
111-
parameters: ParameterDefinitions
112-
113-
def build_parser(self, parser: argparse.ArgumentParser):
114-
parser_state = ParserState()
115-
build_parser(self.parameters, parser, parser_state)
116-
parser.set_defaults(use_case=self, parser_state=parser_state)
117-
118-
def __call__(self, args: argparse.Namespace):
119-
return self.use_case(**get_arguments(self.parameters, args, args.parser_state))
120-
121-
122-
use_cases: Dict[str, _WrappedUseCase] = dict()
101+
use_cases: Dict[str, configurable] = dict()
123102

124103

125-
T = TypeVar("T")
104+
T = TypeVar("T", bound=type)
126105

127106

128107
class AutonomousAgentUseCase(AutonomousUseCase, Generic[T]):
@@ -137,13 +116,12 @@ def get_name(self) -> str:
137116
@classmethod
138117
def __class_getitem__(cls, item):
139118
item = dataclass(item)
140-
item.__parameters__ = get_class_parameters(item)
141119

142120
class AutonomousAgentUseCase(AutonomousUseCase):
143121
agent: Transparent(item) = None
144122

145-
def init(self, configuration):
146-
super().init(configuration)
123+
def init(self):
124+
super().init()
147125
self.agent.init()
148126

149127
def get_name(self) -> str:
@@ -169,7 +147,7 @@ def inner(cls):
169147
name = cls.__name__.removesuffix("UseCase")
170148
if name in use_cases:
171149
raise IndexError(f"Use case with name {name} already exists")
172-
use_cases[name] = _WrappedUseCase(name, description, cls, get_class_parameters(cls))
150+
use_cases[name] = configurable(name, description)(cls)
173151
return cls
174152

175153
return inner
@@ -181,4 +159,4 @@ def register_use_case(name: str, description: str, use_case: Type[UseCase]):
181159
"""
182160
if name in use_cases:
183161
raise IndexError(f"Use case with name {name} already exists")
184-
use_cases[name] = _WrappedUseCase(name, description, use_case, get_class_parameters(use_case))
162+
use_cases[name] = configurable(name, description)(use_case)

‎src/hackingBuddyGPT/usecases/examples/hintfile.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
class ExPrivEscLinuxHintFileUseCase(AutonomousAgentUseCase[LinuxPrivesc]):
99
hints: str = None
1010

11-
def init(self, configuration):
12-
super().init(configuration)
11+
def init(self):
12+
super().init()
1313
self.agent.hint = self.read_hint()
1414

1515
# simple helper that reads the hints file and returns the hint

‎src/hackingBuddyGPT/usecases/rag/linux.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def init(self):
2020
class ThesisLinuxPrivescPrototypeUseCase(AutonomousAgentUseCase[ThesisLinuxPrivescPrototype]):
2121
hints: str = ""
2222

23-
def init(self,configuration):
24-
super().init(configuration)
23+
def init(self):
24+
super().init()
2525
if self.hints != "":
2626
self.agent.hint = self.read_hint()
2727

‎src/hackingBuddyGPT/usecases/viewer.py‎

100755100644
Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from starlette.templating import Jinja2Templates
1919

2020
from hackingBuddyGPT.usecases.base import UseCase, use_case
21+
from hackingBuddyGPT.utils.configurable import parameter
2122
from hackingBuddyGPT.utils.db_storage import DbStorage
2223
from hackingBuddyGPT.utils.db_storage.db_storage import (
2324
Message,
@@ -205,10 +206,9 @@ class Viewer(UseCase):
205206
TODOs:
206207
- [ ] This server needs to be as async as possible to allow good performance, but the database accesses are not yet, might be an issue?
207208
"""
208-
log: GlobalLocalLogger
209-
log_db: DbStorage
210-
listen_host: str = "127.0.0.1"
211-
listen_port: int = 4444
209+
log: GlobalLocalLogger = None
210+
log_db: DbStorage = None
211+
log_server_address: str = "127.0.0.1:4444"
212212
save_playback_dir: str = ""
213213

214214
async def save_message(self, message: ControlMessage):
@@ -232,7 +232,7 @@ async def save_message(self, message: ControlMessage):
232232
with open(file_path, "a") as f:
233233
f.write(ReplayMessage(datetime.datetime.now(), message).to_json() + "\n")
234234

235-
def run(self):
235+
def run(self, config):
236236
@asynccontextmanager
237237
async def lifespan(app: FastAPI):
238238
app.state.db = self.log_db
@@ -337,16 +337,30 @@ async def client_endpoint(websocket: WebSocket):
337337
print("Egress WebSocket disconnected")
338338

339339
import uvicorn
340-
uvicorn.run(app, host=self.listen_host, port=self.listen_port)
340+
listen_parts = self.log_server_address.split(":", 1)
341+
if len(listen_parts) != 2:
342+
if listen_parts[0].startswith("http://"):
343+
listen_parts.append("80")
344+
elif listen_parts[0].startswith("https://"):
345+
listen_parts.append("443")
346+
else:
347+
raise ValueError(f"Invalid log server address (does not contain http/https or a port): {self.log_server_address}")
348+
349+
listen_host, listen_port = listen_parts[0], int(listen_parts[1])
350+
if listen_host.startswith("http://"):
351+
listen_host = listen_host[len("http://"):]
352+
elif listen_host.startswith("https://"):
353+
listen_host = listen_host[len("https://"):]
354+
uvicorn.run(app, host=listen_host, port=listen_port)
341355

342356
def get_name(self) -> str:
343357
return "log_viewer"
344358

345359

346360
@use_case("Tool to replay the .jsonl logs generated by the Viewer (not well tested)")
347361
class Replayer(UseCase):
348-
log: GlobalRemoteLogger
349-
replay_file: str
362+
log: GlobalRemoteLogger = None
363+
replay_file: str = None
350364
pause_on_message: bool = False
351365
pause_on_tool_calls: bool = False
352366
playback_speed: float = 1.0

‎src/hackingBuddyGPT/utils/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .configurable import Configurable, configurable
1+
from .configurable import Configurable, configurable, parameter
22
from .console import *
33
from .db_storage import *
44
from .llm_util import *

0 commit comments

Comments
 (0)