Skip to content

Commit 5c5a6ca

Browse files
committed
add burp to login/navigation sequence converter
1 parent 17a6cc2 commit 5c5a6ca

File tree

2 files changed

+350
-0
lines changed

2 files changed

+350
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Burp to Snyk API&Web Sequence Converter
2+
3+
## Description
4+
5+
Converts Burp Suite Navigation Recorder sequences to Snyk API&Web's sequence format.
6+
7+
This converter transforms recordings made with the Burp Suite Navigation Recorder browser plugin into the format expected by Snyk API&Web for login and navigation sequences.
8+
9+
10+
## Required Options
11+
12+
- `--input/-i`: Path to the Burp recording JSON file
13+
- `--output/-o`: Path to the output Snyk API&Web sequence JSON file
14+
15+
16+
## Usage Examples
17+
18+
### Convert a recorded file to Snyk API&Web sequence
19+
20+
```sh
21+
python3 ./burp_to_saw_sequence_converter.py -i /tmp/burp_recorded_file.json -o /tmp/snyk_login_sequence.json
22+
```
23+
24+
25+
26+
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#!/usr/bin/env python
2+
"""
3+
Converts Burp Suite Navigation Recorder sequences to Snyk API&Web's sequence format.
4+
"""
5+
import argparse
6+
import json
7+
from typing import Dict, List, Any, Optional
8+
from urllib.parse import urlparse
9+
10+
11+
def extract_css_selector(event: Dict[str, Any]) -> Optional[str]:
12+
"""
13+
Generate a CSS selector from Burp event data.
14+
Priority: id > name > placeholder > class > tag
15+
"""
16+
tag = event.get('tagName', '').lower()
17+
if not tag:
18+
return None
19+
20+
# ID
21+
element_id = event.get('id')
22+
if element_id:
23+
return f"#{element_id}"
24+
25+
# Name attribute
26+
name = event.get('name')
27+
if name:
28+
return f"{tag}[name='{name}']"
29+
30+
# Placeholder for inputs
31+
placeholder = event.get('placeholder')
32+
if placeholder and tag == 'input':
33+
return f"input[placeholder='{placeholder}']"
34+
35+
# Class name
36+
class_name = event.get('className', '').strip()
37+
if class_name:
38+
classes = class_name.replace(' ', '.')
39+
return f"{tag}.{classes}"
40+
41+
# href
42+
href = event.get('href')
43+
if href and tag == 'a':
44+
return f"a[href='{href}']"
45+
46+
# Fallback: tag name
47+
return tag
48+
49+
50+
def build_frame_hierarchy(burp_data: List[Dict]) -> Dict[int, Dict]:
51+
"""
52+
Map of all frames
53+
"""
54+
frame_map = {0: {'selector': None, 'parent': None}}
55+
56+
start_event = burp_data[0] if burp_data and burp_data[0].get('eventType') == 'start' else None
57+
if not start_event:
58+
return frame_map
59+
60+
iframes = start_event.get('iframes', [])
61+
for iframe in iframes:
62+
frame_id = iframe.get('frameId')
63+
attrs = iframe.get('attributes', {})
64+
65+
iframe_id = attrs.get('id')
66+
if iframe_id:
67+
selector = f"iframe#{iframe_id}"
68+
elif attrs.get('src'):
69+
selector = f"iframe[src='{attrs.get('src')}']"
70+
elif iframe.get('xPath'):
71+
selector = iframe.get('xPath')
72+
else:
73+
selector = f"iframe:nth-child({iframe.get('iframeIndex', 0) + 1})"
74+
75+
frame_map[frame_id] = {
76+
'selector': selector,
77+
'parent': 0,
78+
'url': attrs.get('src', '')
79+
}
80+
81+
# Discover nested iframes
82+
frame_urls = {}
83+
for event in burp_data:
84+
frame_id = event.get('frameId', 0)
85+
if frame_id != 0 and event.get('isIframe'):
86+
event_url = event.get('url', '')
87+
if event_url:
88+
frame_urls[frame_id] = event_url
89+
90+
# Collect all top-level iframe IDs for combined selector
91+
top_level_iframe_ids = [fid for fid in frame_map.keys() if fid != 0]
92+
93+
for frame_id, url in frame_urls.items():
94+
if frame_id not in frame_map:
95+
parsed = urlparse(url)
96+
path = parsed.path
97+
98+
if path:
99+
path_parts = path.rstrip('/').split('/')
100+
filename = path_parts[-1] if path_parts else path
101+
selector = f"iframe[src*='{filename}']"
102+
else:
103+
selector = "iframe"
104+
105+
# Combine parent selector (all top-level iframes)
106+
frame_map[frame_id] = {
107+
'selector': selector,
108+
'parent': None,
109+
'url': url,
110+
'possible_parents': top_level_iframe_ids
111+
}
112+
113+
return frame_map
114+
115+
116+
def build_frame_selector(frame_map: Dict[int, Dict], frame_id: int) -> Optional[str]:
117+
"""
118+
Build frame selector for iframes
119+
"""
120+
if frame_id == 0:
121+
return None
122+
123+
if frame_id not in frame_map:
124+
return None
125+
126+
frame_info = frame_map.get(frame_id)
127+
if not frame_info:
128+
return None
129+
130+
selector = frame_info.get('selector')
131+
if not selector:
132+
return None
133+
134+
possible_parents = frame_info.get('possible_parents')
135+
if possible_parents:
136+
parent_selectors = []
137+
for parent_id in possible_parents:
138+
parent_info = frame_map.get(parent_id)
139+
if parent_info and parent_info.get('selector'):
140+
parent_selectors.append(parent_info.get('selector'))
141+
142+
if parent_selectors:
143+
# Join selectors with " >>> "
144+
combined_parents = ','.join(parent_selectors)
145+
return f"{combined_parents} >>> {selector}"
146+
else:
147+
return selector
148+
149+
selectors = [selector]
150+
current_frame_id = frame_info.get('parent')
151+
152+
while current_frame_id is not None and current_frame_id != 0:
153+
parent_info = frame_map.get(current_frame_id)
154+
if not parent_info:
155+
break
156+
157+
parent_selector = parent_info.get('selector')
158+
if parent_selector:
159+
selectors.append(parent_selector)
160+
161+
current_frame_id = parent_info.get('parent')
162+
163+
if len(selectors) == 1:
164+
return selectors[0]
165+
166+
selectors.reverse()
167+
return ' >>> '.join(selectors)
168+
169+
170+
def convert_burp_to_saw(burp_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
171+
"""
172+
Convert Burp Suite Navigation Recorder format to SAW sequence format.
173+
"""
174+
saw_sequence = []
175+
176+
if not burp_data or len(burp_data) == 0:
177+
raise Exception('No data in Burp recording file to be converted.')
178+
179+
frame_map = build_frame_hierarchy(burp_data)
180+
181+
for idx, event in enumerate(burp_data):
182+
event_type = event.get('eventType')
183+
184+
# Skip the start item
185+
if event_type == 'start':
186+
continue
187+
188+
# goto
189+
if event_type == 'goto':
190+
url = event.get('url')
191+
timestamp = event.get('timestamp', 0)
192+
window_width = 1800
193+
window_height = 1200
194+
195+
if idx + 1 < len(burp_data):
196+
next_event = burp_data[idx + 1]
197+
window_width = next_event.get('windowInnerWidth', window_width)
198+
window_height = next_event.get('windowInnerHeight', window_height)
199+
200+
saw_sequence.append({
201+
'type': 'goto',
202+
'urlType': 'force',
203+
'url': url,
204+
'timestamp': timestamp,
205+
'windowWidth': window_width,
206+
'windowHeight': window_height
207+
})
208+
209+
# click
210+
elif event_type == 'click':
211+
css = extract_css_selector(event)
212+
xpath = event.get('xPath')
213+
timestamp = event.get('timestamp', 0)
214+
frame_id = event.get('frameId', 0)
215+
216+
frame = build_frame_selector(frame_map, frame_id) if event.get('isIframe') else None
217+
218+
# Value
219+
value = (event.get('value') or event.get('textContent') or '').strip()[:20].replace('\n', '')
220+
221+
saw_sequence.append({
222+
'timestamp': timestamp,
223+
'type': 'click',
224+
'css': css,
225+
'xpath': xpath,
226+
'value': value,
227+
'frame': frame
228+
})
229+
230+
# fill_value
231+
elif event_type == 'typing':
232+
css = extract_css_selector(event)
233+
xpath = event.get('xPath')
234+
typed_value = event.get('typedValue', '')
235+
timestamp = event.get('timestamp', 0)
236+
frame_id = event.get('frameId', 0)
237+
238+
frame = build_frame_selector(frame_map, frame_id) if event.get('isIframe') else None
239+
240+
saw_sequence.append({
241+
'timestamp': timestamp,
242+
'type': 'fill_value',
243+
'css': css,
244+
'xpath': xpath,
245+
'value': typed_value,
246+
'frame': frame
247+
})
248+
249+
# dblclick
250+
elif event_type == 'dblclick':
251+
css = extract_css_selector(event)
252+
xpath = event.get('xPath')
253+
timestamp = event.get('timestamp', 0)
254+
frame_id = event.get('frameId', 0)
255+
256+
frame = build_frame_selector(frame_map, frame_id) if event.get('isIframe') else None
257+
value = (event.get('value') or event.get('textContent') or '').strip()[:20].replace('\n', '')
258+
259+
saw_sequence.append({
260+
'timestamp': timestamp,
261+
'type': 'dblclick',
262+
'css': css,
263+
'xpath': xpath,
264+
'value': value,
265+
'frame': frame
266+
})
267+
268+
# skip userNavigate
269+
elif event_type == 'userNavigate':
270+
pass
271+
272+
# Log unsupported steps
273+
else:
274+
print(f"Warning: Unsupported step '{event_type}' at index {idx}")
275+
276+
return saw_sequence
277+
278+
279+
def run():
280+
"""
281+
Main
282+
"""
283+
parser = argparse.ArgumentParser(
284+
description='Converts Burp Suite Navigation Recorder sequences to Snyk API&Web sequence format'
285+
)
286+
parser.add_argument(
287+
'-i', '--input',
288+
help='Input Burp recording JSON file',
289+
type=argparse.FileType('r'),
290+
required=True
291+
)
292+
parser.add_argument(
293+
'-o', '--output',
294+
help='Output Snyk API&Web sequence JSON file',
295+
type=argparse.FileType('w'),
296+
required=True
297+
)
298+
299+
args = parser.parse_args()
300+
301+
# Load Burp recording
302+
try:
303+
burp_data = json.load(args.input)
304+
except json.JSONDecodeError as e:
305+
print(f"Error: Invalid JSON in input file: {e}")
306+
return 1
307+
308+
# Convert to Snyk API&Web format
309+
try:
310+
saw_data = convert_burp_to_saw(burp_data)
311+
except Exception as e:
312+
print(f"Error during conversion: {e}")
313+
return 1
314+
315+
json.dump(saw_data, args.output, indent=2)
316+
317+
print(f"✓ Successfully converted {len(burp_data)} Burp steps to {len(saw_data)} Snyk API&Web")
318+
print(f"✓ Output written to: {args.output.name}")
319+
320+
return 0
321+
322+
323+
if __name__ == '__main__':
324+
run()

0 commit comments

Comments
 (0)