Skip to content

Commit 71e5eb8

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/web_api_testing' into development_without_spacy
2 parents 676a960 + fb05d87 commit 71e5eb8

8 files changed

Lines changed: 193 additions & 35 deletions

File tree

src/hackingBuddyGPT/capabilities/submit_http_method.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class SubmitHTTPMethod(Capability):
1616
_client = requests.Session()
1717
host: str
1818
follow_redirects: bool = False
19+
success_function: Callable[[], None] = None
1920

2021

2122
submitted_valid_http_methods: Set[str] = field(default_factory=set, init=False)
@@ -67,7 +68,11 @@ def __call__(self, method: Literal["GET", "HEAD", "POST", "PUT", "DELETE", "OPTI
6768
return str(e)
6869

6970
headers = "\r\n".join(f"{k}: {v}" for k, v in resp.headers.items())
70-
71+
if len(self.submitted_valid_http_methods) == len(self.valid_http_methods):
72+
if self.success_function is not None:
73+
self.success_function()
74+
else:
75+
return "All methods submitted, congratulations"
7176
# turn the response into "plain text format" for responding to the prompt
7277
return f"HTTP/1.1 {resp.status_code} {resp.reason}\r\n{headers}\r\n\r\n{resp.text}"""
7378

src/hackingBuddyGPT/cli/wintermute.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ def main():
88
parser = argparse.ArgumentParser()
99
subparser = parser.add_subparsers(required=True)
1010
for name, use_case in use_cases.items():
11-
use_case.build_parser(subparser.add_parser(
11+
subb = subparser.add_parser(
1212
name=use_case.name,
1313
help=use_case.description
14-
))
15-
16-
parsed = parser.parse_args(sys.argv[1:])
14+
)
15+
use_case.build_parser(subb)
16+
x= sys.argv[1:]
17+
parsed = parser.parse_args(x)
1718
instance = parsed.use_case(parsed)
1819
instance.init()
1920
instance.run()

src/hackingBuddyGPT/usecases/web_api_testing/prompt_engineer.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ def __init__(self, strategy, llm_handler, history, schemas, response_handler):
3232
self.response_handler = response_handler
3333
self.llm_handler = llm_handler
3434
self.round = 0
35-
self.found_endpoints = []
35+
self.found_endpoints = ["/"]
36+
self.endpoint_methods = {}
37+
self.endpoint_found_methods = {}
3638
model_name = "en_core_web_sm"
3739

3840
# Check if the model is already installed
@@ -105,7 +107,7 @@ def get_http_action_template(self, method):
105107

106108
else:
107109
return (
108-
f"Create HTTPRequests of type {method} considering the found schemas: {self.schemas} and understand the responses. Ensure that they are correct requests.")
110+
f"Create HTTPRequests of type {method} considering only the object with id=1 for the endpoint and understand the responses. Ensure that they are correct requests.")
109111

110112

111113
def chain_of_thought(self, doc=False, hint=""):
@@ -129,34 +131,56 @@ def chain_of_thought(self, doc=False, hint=""):
129131
"Make the OpenAPI specification available to developers by incorporating it into your API documentation site and keep the documentation up to date with API changes."
130132
]
131133

132-
http_methods = [ "POST", "DELETE", "PUT"]
134+
http_methods = [ "PUT", "DELETE"]
133135
http_phase = {
134-
6: http_methods[0],
135-
16: http_methods[1], # Delete one of instance of this:{self.llm_handler.get_created_objects()}",
136-
20: http_methods[2]
136+
5: http_methods[0],
137+
10: http_methods[1]
137138
}
138139

139140
if doc:
140-
if self.round <= 5:
141+
if self.round < 5:
141142

142143
chain_of_thought_steps = [
143144
f"Identify all available endpoints via GET Requests. Exclude those in this list: {self.found_endpoints}", f"Note down the response structures, status codes, and headers for each endpoint.",
144145
f"For each endpoint, document the following details: URL, HTTP method, "
145146
f"query parameters and path variables, expected request body structure for requests, response structure for successful and error responses."
146147
] + common_steps
147148
else:
148-
phase = http_phase.get(min(filter(lambda x: self.round <= x, http_phase.keys())))
149-
print(f'phase:{phase}')
150-
if phase != "DELETE":
151-
chain_of_thought_steps = [
152-
f"Identify all valid calls for HTTP method {phase}.",
149+
if self.round <= 10:
150+
phase = http_phase.get(min(filter(lambda x: self.round <= x, http_phase.keys())))
151+
print(f'phase:{phase}')
152+
if phase != "DELETE":
153+
chain_of_thought_steps = [
154+
f"Identify for all endpoints {self.found_endpoints} excluding {self.endpoint_found_methods[phase]} a valid HTTP method {phase} call.",
153155
self.get_http_action_template(phase)
154156
] + common_steps
155-
else:
156-
chain_of_thought_steps = [
157+
else:
158+
chain_of_thought_steps = [
157159
f"Check for all endpoints the DELETE method. Delete the first instance for all endpoints. ",
158160
self.get_http_action_template(phase)
159161
] + common_steps
162+
else:
163+
endpoints_needing_help = []
164+
endpoints_and_needed_methods = {}
165+
166+
# Standard HTTP methods
167+
http_methods = {"GET", "POST", "PUT", "DELETE"}
168+
169+
for endpoint in self.endpoint_methods:
170+
# Calculate the missing methods for the current endpoint
171+
missing_methods = http_methods - set(self.endpoint_methods[endpoint])
172+
173+
if len(self.endpoint_methods[endpoint]) < 4:
174+
endpoints_needing_help.append(endpoint)
175+
# Add the missing methods to the dictionary
176+
endpoints_and_needed_methods[endpoint] = list(missing_methods)
177+
178+
print(f'endpoints_and_needed_methods: {endpoints_and_needed_methods}')
179+
print(f'first endpoint in list: {endpoints_needing_help[0]}')
180+
print(f'methods needed for first endpoint: {endpoints_and_needed_methods[endpoints_needing_help[0]][0]}')
181+
182+
chain_of_thought_steps = [f"For enpoint {endpoints_needing_help[0]} find this missing method :{endpoints_and_needed_methods[endpoints_needing_help[0]][0]} "
183+
f"If all the HTTP methods have already been found for an endpoint, then do not include this endpoint in your search. ",]
160184

161185
else:
162186
if self.round == 0:
@@ -187,6 +211,7 @@ def token_count(self, text):
187211
doc = self.nlp(text)
188212
# Count tokens that aren't punctuation marks
189213
tokens = [token for token in doc if not token.is_punct]
214+
print(f'TOKENS: {len(tokens)}')
190215
return len(tokens)
191216

192217

src/hackingBuddyGPT/usecases/web_api_testing/simple_openapi_documentation.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,23 @@ def _setup_initial_prompt(self):
7575

7676

7777
def all_http_methods_found(self):
78-
self._log.console.print(Panel("All HTTP methods found! Congratulations!", title="system"))
79-
self._all_http_methods_found = True
78+
print(f'found endpoints:{self.documentation_handler.endpoint_methods.items()}')
79+
print(f'found endpoints values:{self.documentation_handler.endpoint_methods.values()}')
80+
81+
found_endpoints = sum(len(value_list) for value_list in self.documentation_handler.endpoint_methods.values())
82+
expected_endpoints = len(self.documentation_handler.endpoint_methods.keys())*4
83+
print(f'found endpoints:{found_endpoints}')
84+
print(f'expected endpoints:{expected_endpoints}')
85+
print(f'correct? {found_endpoints== expected_endpoints}')
86+
if found_endpoints== expected_endpoints or found_endpoints == expected_endpoints -1:
87+
return True
88+
else:
89+
return False
8090

8191
def perform_round(self, turn: int):
8292
prompt = self.prompt_engineer.generate_prompt(doc=True)
8393
response, completion = self.llm_handler.call_llm(prompt)
84-
self._handle_response(completion, response)
94+
return self._handle_response(completion, response)
8595

8696
def _handle_response(self, completion, response):
8797
message = completion.choices[0].message
@@ -101,8 +111,17 @@ def _handle_response(self, completion, response):
101111
self.prompt_engineer.found_endpoints = self.documentation_handler.update_openapi_spec(response, result)
102112
self.documentation_handler.write_openapi_to_yaml()
103113
self.prompt_engineer.schemas = self.documentation_handler.schemas
114+
from collections import defaultdict
115+
http_methods_dict = defaultdict(list)
116+
117+
# Iterate through the original dictionary
118+
for endpoint, methods in self.documentation_handler.endpoint_methods.items():
119+
for method in methods:
120+
http_methods_dict[method].append(endpoint)
121+
self.prompt_engineer.endpoint_found_methods = http_methods_dict
122+
self.prompt_engineer.endpoint_methods = self.documentation_handler.endpoint_methods
104123
print(f'SCHEMAS:{self.prompt_engineer.schemas}')
105-
return self._all_http_methods_found
124+
return self.all_http_methods_found()
106125

107126

108127

src/hackingBuddyGPT/usecases/web_api_testing/simple_web_api_testing.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _setup_capabilities(self):
9595
methods_set = {self.http_method_template.format(method=method) for method in self.http_methods.split(",")}
9696
notes = self._context["notes"]
9797
self._capabilities = {
98-
"submit_http_method": SubmitHTTPMethod(self.http_method_description, methods_set, self.host),
98+
"submit_http_method": HTTPRequest(self.host),
9999
"http_request": HTTPRequest(self.host),
100100
"record_note": RecordNote(notes)
101101
}
@@ -134,8 +134,7 @@ def _handle_response(self, completion, response):
134134
result_str = self.response_handler.parse_http_status_line(result)
135135
self._prompt_history.append(tool_message(result_str, tool_call_id))
136136

137-
return self._all_http_methods_found
138-
137+
return self.all_http_methods_found()
139138
@use_case("Minimal implementation of a web API testing use case")
140139
class SimpleWebAPITestingUseCase(AutonomousAgentUseCase[SimpleWebAPITesting]):
141140
pass

src/hackingBuddyGPT/usecases/web_api_testing/utils/documentation_handler.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self, llm_handler, response_handler):
2929
"""
3030
self.response_handler = response_handler
3131
self.schemas = {}
32+
self.endpoint_methods ={}
3233
self.filename = f"openapi_spec_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.yaml"
3334
self.openapi_spec = {
3435
"openapi": "3.0.0",
@@ -50,6 +51,9 @@ def __init__(self, llm_handler, response_handler):
5051
"yaml": YAMLFile()
5152
}
5253

54+
def partial_match(self, element, string_list):
55+
return any(element in string or string in element for string in string_list)
56+
5357
def update_openapi_spec(self, resp, result):
5458
"""
5559
Updates the OpenAPI specification based on the API response provided.
@@ -67,31 +71,51 @@ def update_openapi_spec(self, resp, result):
6771
method = request.method
6872
print(f'method: {method}')
6973
# Ensure that path and method are not None and method has no numeric characters
74+
# Ensure path and method are valid and method has no numeric characters
7075
if path and method:
76+
endpoint_methods = self.endpoint_methods
77+
endpoints = self.openapi_spec['endpoints']
78+
x = path.split('/')[1]
79+
7180
# Initialize the path if not already present
72-
if path not in self.openapi_spec['endpoints']:
73-
self.openapi_spec['endpoints'][path] = {}
81+
if path not in endpoints and x != "":
82+
endpoints[path] = {}
83+
if '1' not in path:
84+
endpoint_methods[path] = []
85+
7486
# Update the method description within the path
75-
example, reference, self.openapi_spec = self.response_handler.parse_http_response_to_openapi_example(self.openapi_spec, result, path, method)
87+
example, reference, self.openapi_spec = self.response_handler.parse_http_response_to_openapi_example(
88+
self.openapi_spec, result, path, method
89+
)
7690
self.schemas = self.openapi_spec["components"]["schemas"]
77-
if example is not None or reference is not None:
78-
self.openapi_spec['endpoints'][path][method.lower()] = {
91+
92+
if example or reference:
93+
endpoints[path][method.lower()] = {
7994
"summary": f"{method} operation on {path}",
8095
"responses": {
8196
"200": {
8297
"description": "Successful response",
8398
"content": {
8499
"application/json": {
85-
"schema": {
86-
"$ref": reference
87-
},
100+
"schema": {"$ref": reference},
88101
"examples": example
89102
}
90103
}
91104
}
92105
}
93106
}
94-
return list(self.openapi_spec['endpoints'].keys())
107+
108+
if '1' not in path and x != "":
109+
endpoint_methods[path].append(method)
110+
elif self.partial_match(x, endpoints.keys()):
111+
path = f"/{x}"
112+
print(f'endpoint methods = {endpoint_methods}')
113+
print(f'new path:{path}')
114+
endpoint_methods[path].append(method)
115+
116+
endpoint_methods[path] = list(set(endpoint_methods[path]))
117+
118+
return list(endpoints.keys())
95119

96120
def write_openapi_to_yaml(self):
97121
"""
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import argparse
2+
import unittest
3+
from hackingBuddyGPT.usecases.base import use_cases
4+
from hackingBuddyGPT.usecases.web_api_testing.simple_web_api_testing import SimpleWebAPITestingUseCase
5+
from hackingBuddyGPT.utils import DbStorage, Console
6+
7+
8+
9+
class WebAPIDocumentationTestCase(unittest.TestCase):
10+
def test_simple_web_api_testing(self):
11+
12+
log_db = DbStorage(':memory:')
13+
console = Console()
14+
15+
log_db.init()
16+
parser = argparse.ArgumentParser()
17+
subparser = parser.add_subparsers(required=True)
18+
for name, use_case in use_cases.items():
19+
use_case.build_parser(subparser.add_parser(
20+
name=use_case.name,
21+
help=use_case.description
22+
))
23+
24+
parsed = parser.parse_args(["SimpleWebAPIDocumentation"])
25+
instance = parsed.use_case(parsed)
26+
27+
agent = instance.agent
28+
simple_web_api_documentation = SimpleWebAPITestingUseCase(
29+
agent=agent,
30+
log_db=log_db,
31+
console=console,
32+
tag='web_api_documentation',
33+
max_turns=20
34+
)
35+
36+
simple_web_api_documentation.init()
37+
result = simple_web_api_documentation.run()
38+
print(f'result: {result}')
39+
assert result is True
40+
41+
42+
if __name__ == '__main__':
43+
unittest.main()

tests/test_web_api_testing.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import argparse
2+
import unittest
3+
from hackingBuddyGPT.usecases.base import use_cases
4+
from hackingBuddyGPT.usecases.web_api_testing.simple_web_api_testing import SimpleWebAPITestingUseCase
5+
from hackingBuddyGPT.utils import DbStorage, Console
6+
7+
8+
9+
class WebAPITestingTestCase(unittest.TestCase):
10+
def test_simple_web_api_testing(self):
11+
12+
log_db = DbStorage(':memory:')
13+
console = Console()
14+
15+
log_db.init()
16+
parser = argparse.ArgumentParser()
17+
subparser = parser.add_subparsers(required=True)
18+
for name, use_case in use_cases.items():
19+
use_case.build_parser(subparser.add_parser(
20+
name=use_case.name,
21+
help=use_case.description
22+
))
23+
24+
parsed = parser.parse_args(["SimpleWebAPITesting"])
25+
instance = parsed.use_case(parsed)
26+
27+
agent = instance.agent
28+
simple_web_api_testing = SimpleWebAPITestingUseCase(
29+
agent=agent,
30+
log_db=log_db,
31+
console=console,
32+
tag='web_api_testing',
33+
max_turns=20
34+
)
35+
36+
simple_web_api_testing.init()
37+
result = simple_web_api_testing.run()
38+
# TODO: find condition for testing
39+
40+
41+
if __name__ == '__main__':
42+
unittest.main()

0 commit comments

Comments
 (0)