Skip to content

Commit e7cfbd9

Browse files
committed
Improve PoC editor
- Change internal tree representation for experiments - Store python backend logic as files without folder - Existing url_queue.txt files can be editor through web UI
1 parent bd0e23b commit e7cfbd9

File tree

6 files changed

+215
-115
lines changed

6 files changed

+215
-115
lines changed

bci/evaluations/custom/custom_evaluation.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,29 @@ def __init__(self):
2020

2121
@staticmethod
2222
def initialize_dir_tree() -> dict:
23+
"""
24+
Initializes directory tree of experiments.
25+
"""
2326
path = Global.custom_page_folder
24-
25-
def path_to_dict(path):
26-
if os.path.isdir(path):
27-
return {
28-
sub_folder: path_to_dict(os.path.join(path, sub_folder))
29-
for sub_folder in os.listdir(path)
30-
if sub_folder != 'url_queue.txt'
31-
}
27+
dir_tree = {}
28+
29+
def set_nested_value(d: dict, keys: list[str], value: dict):
30+
nested_dict = d
31+
for key in keys[:-1]:
32+
nested_dict = nested_dict[key]
33+
nested_dict[keys[-1]] = value
34+
35+
for root, dirs, files in os.walk(path):
36+
# Remove base path from root
37+
root = root[len(path):]
38+
keys = root.split('/')[1:]
39+
subdir_tree = {dir: {} for dir in dirs} | {file: None for file in files}
40+
if root:
41+
set_nested_value(dir_tree, keys, subdir_tree)
3242
else:
33-
return os.path.basename(path)
43+
dir_tree = subdir_tree
3444

35-
return path_to_dict(path)
45+
return dir_tree
3646

3747
@staticmethod
3848
def initialize_tests_and_url_queues(dir_tree: dict) -> dict:
@@ -70,11 +80,14 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> list[st
7080
raise AttributeError(f"Could not infer url queue for experiment '{experiment}' in project '{project}'")
7181

7282
@staticmethod
73-
def is_runnable_experiment(project: str, poc: str, dir_tree: dict) -> bool:
83+
def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str,dict]) -> bool:
7484
domains = dir_tree[project][poc]
75-
if not (poc_main_path := [paths for domain, paths in domains.items() if 'main' in paths]):
85+
# Should have exactly one main folder
86+
main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()]
87+
if len(main_paths) != 1:
7688
return False
77-
if 'index.html' not in poc_main_path[0]['main'].keys():
89+
# Main should have index.html
90+
if 'index.html' not in main_paths[0]['main'].keys():
7891
return False
7992
return True
8093

@@ -194,7 +207,7 @@ def get_default_file_content(file_type: str) -> str:
194207
if file_type != 'py':
195208
return ''
196209

197-
with open('experiments/pages/Support/PythonServer/a.test/py-server/index.py', 'r') as file:
210+
with open('experiments/pages/Support/PythonServer/a.test/server.py', 'r') as file:
198211
template_content = file.read()
199212

200213
return template_content

bci/web/blueprints/api.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
THREAD = None
1717

1818

19-
def start_thread(func, args=None) -> bool:
19+
def __start_thread(func, args=None) -> bool:
2020
global THREAD
21+
if args is None:
22+
args = []
2123
if THREAD and THREAD.is_alive():
2224
return False
2325
else:
@@ -26,7 +28,6 @@ def start_thread(func, args=None) -> bool:
2628
return True
2729

2830

29-
3031
@api.before_request
3132
def check_readiness():
3233
if not bci_api.is_ready():
@@ -55,9 +56,15 @@ def add_headers(response):
5556

5657
@api.route('/evaluation/start/', methods=['POST'])
5758
def start_evaluation():
59+
if request.json is None:
60+
return {
61+
'status': 'NOK',
62+
'msg': "No evaluation parameters found"
63+
}
64+
5865
data = request.json.copy()
5966
params = evaluation_factory(data)
60-
if start_thread(bci_api.run, args=[params]):
67+
if __start_thread(bci_api.run, args=[params]):
6168
return {
6269
'status': 'OK'
6370
}
@@ -69,6 +76,12 @@ def start_evaluation():
6976

7077
@api.route('/evaluation/stop/', methods=['POST'])
7178
def stop_evaluation():
79+
if request.json is None:
80+
return {
81+
'status': 'NOK',
82+
'msg': "No stop parameters found"
83+
}
84+
7285
data = request.json.copy()
7386
forcefully = data.get('forcefully', False)
7487
if forcefully:
@@ -140,6 +153,12 @@ def log():
140153

141154
@api.route('/data/', methods=['PUT'])
142155
def data_source():
156+
if request.json is None:
157+
return {
158+
'status': 'NOK',
159+
'msg': "No data parameters found"
160+
}
161+
143162
params = request.json.copy()
144163
revision_data, version_data = bci_api.get_data_sources(params)
145164
if revision_data or version_data:
@@ -172,30 +191,41 @@ def poc(project: str, poc: str):
172191
}
173192

174193

175-
@api.route('/poc/<string:project>/<string:poc>/<string:domain>/<string:path>/<string:file>/', methods=['GET'])
176-
def poc_file(project: str, poc: str, domain: str, path: str, file: str):
177-
return {
178-
'status': 'OK',
179-
'content': bci_api.get_poc_file(project, poc, domain, path, file)
180-
}
181-
182-
183-
@api.route('/poc/<string:project>/<string:poc>/<string:domain>/<string:path>/<string:file>/', methods=['POST'])
184-
def update_poc_file(project: str, poc: str, domain: str, path: str, file: str):
185-
data = request.json.copy()
186-
success = bci_api.update_poc_file(project, poc, domain, path, file, data['content'])
187-
if success:
194+
@api.route('/poc/<string:project>/<string:poc>/<string:file>/', methods=['GET', 'POST'])
195+
def get_poc_file_content(project: str, poc: str, file: str):
196+
domain = request.args.get('domain', '')
197+
path = request.args.get('path', '')
198+
if request.method == 'GET':
188199
return {
189-
'status': 'OK'
190-
}
191-
else :
192-
return {
193-
'status': 'NOK'
200+
'status': 'OK',
201+
'content': bci_api.get_poc_file(project, poc, domain, path, file)
194202
}
203+
else:
204+
if not request.json:
205+
return {
206+
'status': 'NOK',
207+
'msg': 'No content to update file with'
208+
}
209+
data = request.json.copy()
210+
success = bci_api.update_poc_file(project, poc, domain, path, file, data['content'])
211+
if success:
212+
return {
213+
'status': 'OK'
214+
}
215+
else :
216+
return {
217+
'status': 'NOK'
218+
}
195219

196220

197221
@api.route('/poc/<string:project>/<string:poc>/', methods=['POST'])
198222
def add_page(project: str, poc: str):
223+
if request.json is None:
224+
return {
225+
'status': 'NOK',
226+
'msg': "No page parameters found"
227+
}
228+
199229
data = request.json.copy()
200230
success = bci_api.add_page(project, poc, data['domain'], data['page'], data['file_type'])
201231
if success:
@@ -218,6 +248,12 @@ def get_available_domains():
218248

219249
@api.route('/poc/<string:project>/', methods=['POST'])
220250
def create_experiment(project: str):
251+
if request.json is None:
252+
return {
253+
'status': 'NOK',
254+
'msg': "No experiment parameters found"
255+
}
256+
221257
data = request.json.copy()
222258
if 'poc_name' not in data.keys():
223259
return {

bci/web/blueprints/experiments.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66

77
import requests
8-
from flask import Blueprint, make_response, render_template, request, url_for
8+
from flask import Blueprint, Request, make_response, render_template, request, url_for
99

1010
from bci.web.page_parser import load_experiment_pages
1111

@@ -26,6 +26,7 @@
2626

2727
@exp.before_request
2828
def before_request():
29+
__report(request)
2930
host = request.host.lower()
3031
if host not in ALLOWED_DOMAINS:
3132
logger.error(
@@ -34,29 +35,13 @@ def before_request():
3435
return f"Host '{host}' is not supported by this framework."
3536

3637

37-
@exp.route("/")
38-
def index():
39-
return f"This page is visited over <b>{request.scheme}</b>."
40-
41-
42-
@exp.route("/report/", methods=["GET", "POST"])
43-
def report():
44-
get_params = [item for item in get_all_GET_parameters(request).items()]
45-
resp = make_response(
46-
render_template("cookies.html", title="Report", get_params=get_params)
47-
)
48-
49-
cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4)
50-
resp.set_cookie("generic", "1", expires=cookie_exp_date)
51-
resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True)
52-
resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True)
53-
resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax")
54-
resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict")
55-
38+
def __report(request: Request) -> None:
39+
"""
40+
Submit report to BugHog
41+
"""
5642
# Respond to collector on same IP
5743
# remote_ip = request.remote_addr
5844
remote_ip = request.headers.get("X-Real-IP")
59-
6045
response_data = {
6146
"url": request.url,
6247
"method": request.method,
@@ -72,6 +57,29 @@ def send_report_to_collector():
7257

7358
threading.Thread(target=send_report_to_collector).start()
7459

60+
61+
def __get_all_GET_parameters(request) -> dict[str,str]:
62+
return {k: v for k, v in request.args.items()}
63+
64+
65+
@exp.route("/")
66+
def index():
67+
return f"This page is visited over <b>{request.scheme}</b>."
68+
69+
70+
@exp.route("/report/", methods=["GET", "POST"])
71+
def report_endpoint():
72+
get_params = [item for item in __get_all_GET_parameters(request).items()]
73+
resp = make_response(
74+
render_template("cookies.html", title="Report", get_params=get_params)
75+
)
76+
77+
cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4)
78+
resp.set_cookie("generic", "1", expires=cookie_exp_date)
79+
resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True)
80+
resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True)
81+
resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax")
82+
resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict")
7583
return resp
7684

7785

@@ -81,7 +89,7 @@ def report_leak_if_using_http(target_scheme):
8189
Triggers request to /report/ if a request was received over the specified `scheme`.
8290
"""
8391
used_scheme = request.headers.get("X-Forwarded-Proto")
84-
params = get_all_GET_parameters(request)
92+
params = __get_all_GET_parameters(request)
8593
if used_scheme == target_scheme:
8694
return "Redirect", 307, {"Location": url_for("experiments.report", **params)}
8795
else:
@@ -96,7 +104,7 @@ def report_leak_if_present(expected_header_name: str):
96104
if expected_header_name not in request.headers:
97105
return f"Header {expected_header_name} not found", 200, {"Allow-CSP-From": "*"}
98106

99-
params = get_all_GET_parameters(request)
107+
params = __get_all_GET_parameters(request)
100108
return (
101109
"Redirect",
102110
307,
@@ -121,7 +129,7 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st
121129
{"Allow-CSP-From": "*"},
122130
)
123131

124-
params = get_all_GET_parameters(request)
132+
params = __get_all_GET_parameters(request)
125133
return (
126134
"Redirect",
127135
307,
@@ -132,15 +140,15 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st
132140
)
133141

134142

135-
@exp.route("/<string:project>/<string:experiment>/<string:directory>/")
136-
def python_evaluation(project: str, experiment: str, directory: str):
143+
@exp.route("/<string:project>/<string:experiment>/<string:file_name>.py")
144+
def python_evaluation(project: str, experiment: str, file_name: str):
137145
"""
138146
Evaluates the python script and returns its result.
139147
"""
140148
host = request.host.lower()
141149

142-
module_name = f"{host}/{project}/{experiment}/{directory}"
143-
path = f"experiments/pages/{project}/{experiment}/{host}/{directory}/index.py"
150+
module_name = f"{host}/{project}/{experiment}"
151+
path = f"experiments/pages/{project}/{experiment}/{host}/{file_name}.py"
144152

145153
# Dynamically import the file
146154
sys.dont_write_bytecode = True
@@ -150,7 +158,3 @@ def python_evaluation(project: str, experiment: str, directory: str):
150158
spec.loader.exec_module(module)
151159

152160
return module.main(request)
153-
154-
155-
def get_all_GET_parameters(request):
156-
return {k: v for k, v in request.args.items()}

0 commit comments

Comments
 (0)