parent
a1093b8cb1
commit
a9c5966109
Binary file not shown.
Binary file not shown.
@ -1,250 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
Converts a mitmproxy dump file to a swagger schema.
|
|
||||||
"""
|
|
||||||
from email import header
|
|
||||||
from mitmproxy import io as iom, http
|
|
||||||
from mitmproxy.exceptions import FlowReadException
|
|
||||||
import pprint
|
|
||||||
import sys
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import ruamel.yaml
|
|
||||||
import re
|
|
||||||
import swagger_util
|
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Converts a mitmproxy dump file to a swagger schema.')
|
|
||||||
parser.add_argument(
|
|
||||||
'-i', '--input', help='The input mitmproxy dump file', required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
'-o', '--output', help='The output swagger schema file (yaml). If it exists, new endpoints will be added', required=True)
|
|
||||||
parser.add_argument('-p', '--api-prefix', help='The api prefix', required=True)
|
|
||||||
parser.add_argument('-e', '--examples', action='store_true',
|
|
||||||
help='Include examples in the schema. This might expose sensitive information.')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
yaml = ruamel.yaml.YAML()
|
|
||||||
|
|
||||||
swagger = None
|
|
||||||
|
|
||||||
# try loading the existing swagger file
|
|
||||||
try:
|
|
||||||
with open(args.output, 'r') as f:
|
|
||||||
swagger = yaml.load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("No existing swagger file found. Creating new one.")
|
|
||||||
pass
|
|
||||||
if swagger is None:
|
|
||||||
swagger = ruamel.yaml.comments.CommentedMap({
|
|
||||||
"openapi": "3.0.0",
|
|
||||||
"info": {
|
|
||||||
"title": args.input + " Mitmproxy2Swagger",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
})
|
|
||||||
# strip the trailing slash from the api prefix
|
|
||||||
args.api_prefix = args.api_prefix.rstrip('/')
|
|
||||||
|
|
||||||
if not 'servers' in swagger or swagger['servers'] is None:
|
|
||||||
swagger['servers'] = []
|
|
||||||
# add the server if it doesn't exist
|
|
||||||
if not any(server['url'] == args.api_prefix for server in swagger['servers']):
|
|
||||||
swagger['servers'].append({
|
|
||||||
"url": args.api_prefix,
|
|
||||||
"description": "The default server"
|
|
||||||
})
|
|
||||||
if not 'paths' in swagger or swagger['paths'] is None:
|
|
||||||
swagger['paths'] = {}
|
|
||||||
if 'x-path-templates' not in swagger or swagger['x-path-templates'] is None:
|
|
||||||
swagger['x-path-templates'] = []
|
|
||||||
|
|
||||||
path_templates = []
|
|
||||||
for path in swagger['paths']:
|
|
||||||
path_templates.append(path)
|
|
||||||
# also add paths from the the x-path-templates array
|
|
||||||
if 'x-path-templates' in swagger and swagger['x-path-templates'] is not None:
|
|
||||||
for path in swagger['x-path-templates']:
|
|
||||||
path_templates.append(path)
|
|
||||||
|
|
||||||
# new endpoints will be added here so that they can be added as comments in the swagger file
|
|
||||||
new_path_templates = []
|
|
||||||
|
|
||||||
|
|
||||||
def path_to_regex(path):
|
|
||||||
# replace the path template with a regex
|
|
||||||
path = path.replace('{', '(?P<')
|
|
||||||
path = path.replace('}', '>[^/]+)')
|
|
||||||
path = path.replace('*', '.*')
|
|
||||||
path = path.replace('/', '\/')
|
|
||||||
return "^" + path + "$"
|
|
||||||
|
|
||||||
|
|
||||||
def strip_query_string(path):
|
|
||||||
# remove the query string from the path
|
|
||||||
return path.split('?')[0]
|
|
||||||
|
|
||||||
|
|
||||||
def set_key_if_not_exists(dict, key, value):
|
|
||||||
if key not in dict:
|
|
||||||
dict[key] = value
|
|
||||||
|
|
||||||
|
|
||||||
path_template_regexes = [re.compile(path_to_regex(path))
|
|
||||||
for path in path_templates]
|
|
||||||
|
|
||||||
|
|
||||||
with open(args.input, 'rb') as logfile:
|
|
||||||
logfile_size = os.path.getsize(args.input)
|
|
||||||
freader = iom.FlowReader(logfile)
|
|
||||||
|
|
||||||
pp = pprint.PrettyPrinter(indent=4)
|
|
||||||
try:
|
|
||||||
for f in freader.stream():
|
|
||||||
sys.stdout.write("Progress {0:.2f}%%\r".format(
|
|
||||||
(logfile.tell() / logfile_size * 100)))
|
|
||||||
# print(f)
|
|
||||||
if isinstance(f, http.HTTPFlow):
|
|
||||||
if not f.request.url.startswith(args.api_prefix):
|
|
||||||
continue
|
|
||||||
# strip the api prefix from the url
|
|
||||||
url = f.request.url[len(args.api_prefix):]
|
|
||||||
method = f.request.method.lower()
|
|
||||||
path = strip_query_string(url)
|
|
||||||
if f.response is None:
|
|
||||||
print("[WARN] No response for " + url)
|
|
||||||
continue
|
|
||||||
status = f.response.status_code
|
|
||||||
|
|
||||||
# check if the path matches any of the path templates, and save the index
|
|
||||||
path_template_index = None
|
|
||||||
for i, path_template_regex in enumerate(path_template_regexes):
|
|
||||||
if path_template_regex.match(path):
|
|
||||||
path_template_index = i
|
|
||||||
break
|
|
||||||
if path_template_index is None:
|
|
||||||
if path in new_path_templates:
|
|
||||||
continue
|
|
||||||
new_path_templates.append(path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
path_template_to_set = path_templates[path_template_index]
|
|
||||||
set_key_if_not_exists(
|
|
||||||
swagger['paths'], path_template_to_set, {})
|
|
||||||
|
|
||||||
|
|
||||||
set_key_if_not_exists(swagger['paths'][path_template_to_set], method, {
|
|
||||||
'summary': swagger_util.path_template_to_endpoint_name(method, path_template_to_set),
|
|
||||||
|
|
||||||
'responses': {}
|
|
||||||
})
|
|
||||||
params = swagger_util.url_to_params(url, path_template_to_set)
|
|
||||||
if params is not None and len(params) > 0:
|
|
||||||
set_key_if_not_exists(swagger['paths'][path_template_to_set][method], 'parameters', params)
|
|
||||||
if method not in ['get', 'head']:
|
|
||||||
body_val = None
|
|
||||||
# try to parse the body as json
|
|
||||||
try:
|
|
||||||
body_val = json.loads(f.request.text)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
if body_val is not None:
|
|
||||||
content_to_set = {
|
|
||||||
'content': {
|
|
||||||
'application/json': {
|
|
||||||
'schema': swagger_util.value_to_schema(body_val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if args.examples:
|
|
||||||
content_to_set['content']['application/json']['example'] = swagger_util.limit_example_size(
|
|
||||||
body_val)
|
|
||||||
set_key_if_not_exists(
|
|
||||||
swagger['paths'][path_template_to_set][method], 'requestBody', content_to_set)
|
|
||||||
# try parsing the response as json
|
|
||||||
try:
|
|
||||||
response_json = json.loads(f.response.text)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
response_json = None
|
|
||||||
if response_json is not None:
|
|
||||||
resp_data_to_set = {
|
|
||||||
'description': f.response.reason,
|
|
||||||
'content': {
|
|
||||||
'application/json': {
|
|
||||||
'schema': swagger_util.value_to_schema(response_json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if args.examples:
|
|
||||||
resp_data_to_set['content']['application/json']['example'] = swagger_util.limit_example_size(
|
|
||||||
response_json)
|
|
||||||
set_key_if_not_exists(swagger['paths'][path_template_to_set][method]['responses'], str(
|
|
||||||
status), resp_data_to_set)
|
|
||||||
|
|
||||||
except FlowReadException as e:
|
|
||||||
print(f"Flow file corrupted: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
new_path_templates.sort()
|
|
||||||
|
|
||||||
# add suggested path templates
|
|
||||||
# basically inspects urls and replaces segments containing only numbers with a parameter
|
|
||||||
new_path_templates_with_suggestions = []
|
|
||||||
for idx, path in enumerate(new_path_templates):
|
|
||||||
# check if path contains number-only segments
|
|
||||||
segments = path.split('/')
|
|
||||||
if any(segment.isdigit() for segment in segments):
|
|
||||||
# replace digit segments with {id}, {id1}, {id2} etc
|
|
||||||
new_segments = []
|
|
||||||
param_id = 0
|
|
||||||
for segment in segments:
|
|
||||||
if segment.isdigit():
|
|
||||||
param_name = 'id' + str(param_id)
|
|
||||||
if param_id == 0:
|
|
||||||
param_name = 'id'
|
|
||||||
new_segments.append('{' + param_name + '}')
|
|
||||||
param_id += 1
|
|
||||||
else:
|
|
||||||
new_segments.append(segment)
|
|
||||||
suggested_path = '/'.join(new_segments)
|
|
||||||
# prepend the suggested path to the new_path_templates list
|
|
||||||
if suggested_path not in new_path_templates_with_suggestions:
|
|
||||||
new_path_templates_with_suggestions.append(
|
|
||||||
"ignore:" + suggested_path)
|
|
||||||
new_path_templates_with_suggestions.append("ignore:" + path)
|
|
||||||
|
|
||||||
# remove the ending comments not to add them twice
|
|
||||||
|
|
||||||
# append the contents of new_path_templates_with_suggestions to swagger['x-path-templates']
|
|
||||||
for path in new_path_templates_with_suggestions:
|
|
||||||
swagger['x-path-templates'].append(path)
|
|
||||||
|
|
||||||
# remove elements already generated
|
|
||||||
swagger['x-path-templates'] = [
|
|
||||||
path for path in swagger['x-path-templates'] if path not in swagger['paths']]
|
|
||||||
|
|
||||||
# remove duplicates while preserving order
|
|
||||||
def f7(seq):
|
|
||||||
seen = set()
|
|
||||||
seen_add = seen.add
|
|
||||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
|
||||||
swagger['x-path-templates'] = f7(swagger['x-path-templates'])
|
|
||||||
|
|
||||||
swagger['x-path-templates'] = ruamel.yaml.comments.CommentedSeq(
|
|
||||||
swagger['x-path-templates'])
|
|
||||||
swagger['x-path-templates'].yaml_set_start_comment(
|
|
||||||
'Remove the ignore: prefix to generate an endpoint with its URL\nLines that are closer to the top take precedence, the matching is greedy')
|
|
||||||
# save the swagger file
|
|
||||||
with open(args.output, 'w') as f:
|
|
||||||
yaml.dump(swagger, f)
|
|
||||||
# f.write(
|
|
||||||
# " # Uncomment any of the following lines to generate the corresponding endpoint\n")
|
|
||||||
# f.write(
|
|
||||||
# " # Lines that are closer to the top take precedence, the matching is greedy\n")
|
|
||||||
# f.write("\n")
|
|
||||||
# for path in new_path_templates_with_suggestions:
|
|
||||||
# f.write(" #- " + path + "\n")
|
|
||||||
print("Done!")
|
|
@ -0,0 +1,250 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Converts a mitmproxy dump file to a swagger schema.
|
||||||
|
"""
|
||||||
|
from email import header
|
||||||
|
from mitmproxy import io as iom, http
|
||||||
|
from mitmproxy.exceptions import FlowReadException
|
||||||
|
import pprint
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import ruamel.yaml
|
||||||
|
import re
|
||||||
|
import mitmproxy2swagger.swagger_util.swagger_util as swagger_util
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def path_to_regex(path):
|
||||||
|
# replace the path template with a regex
|
||||||
|
path = path.replace('{', '(?P<')
|
||||||
|
path = path.replace('}', '>[^/]+)')
|
||||||
|
path = path.replace('*', '.*')
|
||||||
|
path = path.replace('/', '\/')
|
||||||
|
return "^" + path + "$"
|
||||||
|
|
||||||
|
|
||||||
|
def strip_query_string(path):
|
||||||
|
# remove the query string from the path
|
||||||
|
return path.split('?')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def set_key_if_not_exists(dict, key, value):
|
||||||
|
if key not in dict:
|
||||||
|
dict[key] = value
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Converts a mitmproxy dump file to a swagger schema.')
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--input', help='The input mitmproxy dump file', required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
'-o', '--output', help='The output swagger schema file (yaml). If it exists, new endpoints will be added', required=True)
|
||||||
|
parser.add_argument('-p', '--api-prefix', help='The api prefix', required=True)
|
||||||
|
parser.add_argument('-e', '--examples', action='store_true',
|
||||||
|
help='Include examples in the schema. This might expose sensitive information.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
yaml = ruamel.yaml.YAML()
|
||||||
|
|
||||||
|
swagger = None
|
||||||
|
|
||||||
|
# try loading the existing swagger file
|
||||||
|
try:
|
||||||
|
with open(args.output, 'r') as f:
|
||||||
|
swagger = yaml.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("No existing swagger file found. Creating new one.")
|
||||||
|
pass
|
||||||
|
if swagger is None:
|
||||||
|
swagger = ruamel.yaml.comments.CommentedMap({
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": args.input + " Mitmproxy2Swagger",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
# strip the trailing slash from the api prefix
|
||||||
|
args.api_prefix = args.api_prefix.rstrip('/')
|
||||||
|
|
||||||
|
if not 'servers' in swagger or swagger['servers'] is None:
|
||||||
|
swagger['servers'] = []
|
||||||
|
# add the server if it doesn't exist
|
||||||
|
if not any(server['url'] == args.api_prefix for server in swagger['servers']):
|
||||||
|
swagger['servers'].append({
|
||||||
|
"url": args.api_prefix,
|
||||||
|
"description": "The default server"
|
||||||
|
})
|
||||||
|
if not 'paths' in swagger or swagger['paths'] is None:
|
||||||
|
swagger['paths'] = {}
|
||||||
|
if 'x-path-templates' not in swagger or swagger['x-path-templates'] is None:
|
||||||
|
swagger['x-path-templates'] = []
|
||||||
|
|
||||||
|
path_templates = []
|
||||||
|
for path in swagger['paths']:
|
||||||
|
path_templates.append(path)
|
||||||
|
# also add paths from the the x-path-templates array
|
||||||
|
if 'x-path-templates' in swagger and swagger['x-path-templates'] is not None:
|
||||||
|
for path in swagger['x-path-templates']:
|
||||||
|
path_templates.append(path)
|
||||||
|
|
||||||
|
# new endpoints will be added here so that they can be added as comments in the swagger file
|
||||||
|
new_path_templates = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
path_template_regexes = [re.compile(path_to_regex(path))
|
||||||
|
for path in path_templates]
|
||||||
|
|
||||||
|
|
||||||
|
with open(args.input, 'rb') as logfile:
|
||||||
|
logfile_size = os.path.getsize(args.input)
|
||||||
|
freader = iom.FlowReader(logfile)
|
||||||
|
|
||||||
|
pp = pprint.PrettyPrinter(indent=4)
|
||||||
|
try:
|
||||||
|
for f in freader.stream():
|
||||||
|
sys.stdout.write("Progress {0:.2f}%%\r".format(
|
||||||
|
(logfile.tell() / logfile_size * 100)))
|
||||||
|
# print(f)
|
||||||
|
if isinstance(f, http.HTTPFlow):
|
||||||
|
if not f.request.url.startswith(args.api_prefix):
|
||||||
|
continue
|
||||||
|
# strip the api prefix from the url
|
||||||
|
url = f.request.url[len(args.api_prefix):]
|
||||||
|
method = f.request.method.lower()
|
||||||
|
path = strip_query_string(url)
|
||||||
|
if f.response is None:
|
||||||
|
print("[WARN] No response for " + url)
|
||||||
|
continue
|
||||||
|
status = f.response.status_code
|
||||||
|
|
||||||
|
# check if the path matches any of the path templates, and save the index
|
||||||
|
path_template_index = None
|
||||||
|
for i, path_template_regex in enumerate(path_template_regexes):
|
||||||
|
if path_template_regex.match(path):
|
||||||
|
path_template_index = i
|
||||||
|
break
|
||||||
|
if path_template_index is None:
|
||||||
|
if path in new_path_templates:
|
||||||
|
continue
|
||||||
|
new_path_templates.append(path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
path_template_to_set = path_templates[path_template_index]
|
||||||
|
set_key_if_not_exists(
|
||||||
|
swagger['paths'], path_template_to_set, {})
|
||||||
|
|
||||||
|
|
||||||
|
set_key_if_not_exists(swagger['paths'][path_template_to_set], method, {
|
||||||
|
'summary': swagger_util.path_template_to_endpoint_name(method, path_template_to_set),
|
||||||
|
|
||||||
|
'responses': {}
|
||||||
|
})
|
||||||
|
params = swagger_util.url_to_params(url, path_template_to_set)
|
||||||
|
if params is not None and len(params) > 0:
|
||||||
|
set_key_if_not_exists(swagger['paths'][path_template_to_set][method], 'parameters', params)
|
||||||
|
if method not in ['get', 'head']:
|
||||||
|
body_val = None
|
||||||
|
# try to parse the body as json
|
||||||
|
try:
|
||||||
|
body_val = json.loads(f.request.text)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
if body_val is not None:
|
||||||
|
content_to_set = {
|
||||||
|
'content': {
|
||||||
|
'application/json': {
|
||||||
|
'schema': swagger_util.value_to_schema(body_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.examples:
|
||||||
|
content_to_set['content']['application/json']['example'] = swagger_util.limit_example_size(
|
||||||
|
body_val)
|
||||||
|
set_key_if_not_exists(
|
||||||
|
swagger['paths'][path_template_to_set][method], 'requestBody', content_to_set)
|
||||||
|
# try parsing the response as json
|
||||||
|
try:
|
||||||
|
response_json = json.loads(f.response.text)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
response_json = None
|
||||||
|
if response_json is not None:
|
||||||
|
resp_data_to_set = {
|
||||||
|
'description': f.response.reason,
|
||||||
|
'content': {
|
||||||
|
'application/json': {
|
||||||
|
'schema': swagger_util.value_to_schema(response_json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.examples:
|
||||||
|
resp_data_to_set['content']['application/json']['example'] = swagger_util.limit_example_size(
|
||||||
|
response_json)
|
||||||
|
set_key_if_not_exists(swagger['paths'][path_template_to_set][method]['responses'], str(
|
||||||
|
status), resp_data_to_set)
|
||||||
|
|
||||||
|
except FlowReadException as e:
|
||||||
|
print(f"Flow file corrupted: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
new_path_templates.sort()
|
||||||
|
|
||||||
|
# add suggested path templates
|
||||||
|
# basically inspects urls and replaces segments containing only numbers with a parameter
|
||||||
|
new_path_templates_with_suggestions = []
|
||||||
|
for idx, path in enumerate(new_path_templates):
|
||||||
|
# check if path contains number-only segments
|
||||||
|
segments = path.split('/')
|
||||||
|
if any(segment.isdigit() for segment in segments):
|
||||||
|
# replace digit segments with {id}, {id1}, {id2} etc
|
||||||
|
new_segments = []
|
||||||
|
param_id = 0
|
||||||
|
for segment in segments:
|
||||||
|
if segment.isdigit():
|
||||||
|
param_name = 'id' + str(param_id)
|
||||||
|
if param_id == 0:
|
||||||
|
param_name = 'id'
|
||||||
|
new_segments.append('{' + param_name + '}')
|
||||||
|
param_id += 1
|
||||||
|
else:
|
||||||
|
new_segments.append(segment)
|
||||||
|
suggested_path = '/'.join(new_segments)
|
||||||
|
# prepend the suggested path to the new_path_templates list
|
||||||
|
if suggested_path not in new_path_templates_with_suggestions:
|
||||||
|
new_path_templates_with_suggestions.append(
|
||||||
|
"ignore:" + suggested_path)
|
||||||
|
new_path_templates_with_suggestions.append("ignore:" + path)
|
||||||
|
|
||||||
|
# remove the ending comments not to add them twice
|
||||||
|
|
||||||
|
# append the contents of new_path_templates_with_suggestions to swagger['x-path-templates']
|
||||||
|
for path in new_path_templates_with_suggestions:
|
||||||
|
swagger['x-path-templates'].append(path)
|
||||||
|
|
||||||
|
# remove elements already generated
|
||||||
|
swagger['x-path-templates'] = [
|
||||||
|
path for path in swagger['x-path-templates'] if path not in swagger['paths']]
|
||||||
|
|
||||||
|
# remove duplicates while preserving order
|
||||||
|
def f7(seq):
|
||||||
|
seen = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||||
|
swagger['x-path-templates'] = f7(swagger['x-path-templates'])
|
||||||
|
|
||||||
|
swagger['x-path-templates'] = ruamel.yaml.comments.CommentedSeq(
|
||||||
|
swagger['x-path-templates'])
|
||||||
|
swagger['x-path-templates'].yaml_set_start_comment(
|
||||||
|
'Remove the ignore: prefix to generate an endpoint with its URL\nLines that are closer to the top take precedence, the matching is greedy')
|
||||||
|
# save the swagger file
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
yaml.dump(swagger, f)
|
||||||
|
print("Done!")
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in new issue