diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 86d2541a..4b3dd321 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Current - Add: new `listen` and `listen_status` options in burp-2.2.10 `#279 `_ - Add: new `order` keyword in ACL definitions in order to decide whether `rw` should be evaluated first or not `#305 `__ - Add: new `exclude` keyword in ACL definitions in order to exclude some clients from the rules `#305 `__ +- Add: new *static templates* that allow you to create *onetime* (variables) templates `#280 `_ - Add: allow to hide selected clients/servers `#282 `_ - Add: allow to delete clients data upon removal `#232 `_ - Add: allow to create clients from templates in one call `#266 `_ diff --git a/burpui/api/settings.py b/burpui/api/settings.py index 3a6dbe07..62e6bd63 100644 --- a/burpui/api/settings.py +++ b/burpui/api/settings.py @@ -7,6 +7,8 @@ .. moduleauthor:: Ziirish """ +import json + from . import api from ..engines.server import BUIServer # noqa from ..ext.cache import cache @@ -15,11 +17,14 @@ from .._compat import unquote from ..utils import NOTIF_INFO from flask_babel import gettext as _, refresh -from flask import jsonify, request, url_for, current_app, g, session +from flask import jsonify, request, url_for, current_app, g, session, render_template_string from flask_login import current_user from flask_restplus import inputs +from jinja2 import Environment, meta from ..datastructures import ImmutableMultiDict, MultiDict +TEMPLATE_EXCLUDES = ['client', 'agent'] + bui = current_app # type: BUIServer ns = api.namespace('settings', 'Settings methods') @@ -323,6 +328,89 @@ class ClientsList(Resource): return jsonify(result=res) +@ns.route('/static-templates', + '//static-templates', + endpoint='static_templates_list') +@ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + }, +) +class StaticTemplatesList(Resource): + + @api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel') + @ns.doc( + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) + def get(self, server=None): + """Returns a list of clients""" + parser = bui.client.get_parser(agent=server) + res = parser.list_static_templates() + env = Environment() + for obj in res: + ast = env.parse(obj['content']) + obj['variables'] = [x for x in meta.find_undeclared_variables(ast) if x not in TEMPLATE_EXCLUDES] + return jsonify(result=res) + + +@ns.route('/static-template', + '//static-template', + endpoint='new_static_template', + methods=['PUT']) +@ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + }, +) +class NewStaticTemplateSettings(Resource): + parser = ns.parser() + parser.add_argument('newstatictemplate', required=True, help="No 'newstatictemplate' provided") + + @api.disabled_on_demo() + @api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel') + @ns.expect(parser) + @ns.doc( + responses={ + 200: 'Success', + 400: 'Missing parameter', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) + def put(self, server=None): + """Creates a new template""" + if not current_user.is_anonymous and \ + current_user.acl.is_moderator() and \ + not current_user.acl.is_server_rw(server): + self.abort(403, 'You don\'t have rights on this server') + + newtemplate = self.parser.parse_args()['newstatictemplate'] + if not newtemplate: + self.abort(400, 'No template name provided') + parser = bui.client.get_parser(agent=server) + templates = parser.list_static_templates() + if any(tpl['name'] == newtemplate for tpl in templates): + self.abort(409, "Static template '{}' already exists".format(newtemplate)) + # clientconfdir = bui.client.get_parser_attr('clientconfdir', server) + # if not clientconfdir: + # flash('Could not proceed, no \'clientconfdir\' find', 'warning') + # return redirect(request.referrer) + noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, False, True, server) + if server: + url = url_for('view.cli_settings', server=server, client=newtemplate, statictemplate=True) + else: + url = url_for('view.cli_settings', client=newtemplate, statictemplate=True) + noti.append([NOTIF_INFO, _('Click here to edit \'%(template)s\' configuration', url=url, template=newtemplate)]) + # clear the cache when we add a new client + cache.clear() + bui.audit.logger.info(f'created new static template {newtemplate}', server=server) + return {'notif': noti}, 201 + + @ns.route('/templates', '//templates', endpoint='templates_list') @@ -391,7 +479,7 @@ class NewTemplateSettings(Resource): # if not clientconfdir: # flash('Could not proceed, no \'clientconfdir\' find', 'warning') # return redirect(request.referrer) - noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, True, server) + noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, True, False, server) if server: url = url_for('view.cli_settings', server=server, client=newtemplate, template=True) else: @@ -416,6 +504,8 @@ class NewClientSettings(Resource): parser = ns.parser() parser.add_argument('newclient', required=True, help="No 'newclient' provided") parser.add_argument('templates', help="Templates list", action='split') + parser.add_argument('statictemplate', help="Static template") + parser.add_argument('variables', help="Template variables") @api.disabled_on_demo() @api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel') @@ -433,6 +523,10 @@ class NewClientSettings(Resource): args = self.parser.parse_args() newclient = args['newclient'] templates = [x for x in args.get('templates', []) if x] + statictemplate = args['statictemplate'] + variables = json.loads(args['variables']) if args['variables'] else {} + variables['agent'] = server + variables['client'] = newclient if not newclient: self.abort(400, 'No client name provided') @@ -451,12 +545,18 @@ class NewClientSettings(Resource): # flash('Could not proceed, no \'clientconfdir\' find', 'warning') # return redirect(request.referrer) data = MultiDict() + content = '' if templates: real_templates = {x['name']: x['value'] for x in parser._list_templates()} if any(x not in real_templates for x in templates): self.abort(400, 'Wrong template') data.setlist('templates', [real_templates[x] for x in templates]) - noti = bui.client.store_conf_cli(ImmutableMultiDict(data), newclient, None, agent=server) + if statictemplate: + statics = parser._list_static_templates() + for tpl in statics: + if tpl['name'] == statictemplate: + content = render_template_string(tpl['content'], **variables) + noti = bui.client.store_conf_cli(ImmutableMultiDict(data), newclient, None, content=content, agent=server) if server: url = url_for('view.cli_settings', server=server, client=newclient) else: @@ -498,16 +598,20 @@ class ClientSettings(Resource): parser_delete.add_argument('delcert', type=inputs.boolean, help='Whether to delete the certificate or not', default=False, nullable=True) parser_delete.add_argument('keepconf', type=inputs.boolean, help='Whether to keep the conf or not', default=False, nullable=True) parser_delete.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True) + parser_delete.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True) parser_delete.add_argument('delete', type=inputs.boolean, help='Whether we should remove the data as well or not', default=False, nullable=True) parser_put = ns.parser() parser_put.add_argument('newname', help='New name of the client/template') parser_put.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True) + parser_put.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True) parser_put.add_argument('keepcert', type=inputs.boolean, help='Whether to keep the same certificate or not', default=False, nullable=True) parser_put.add_argument('keepdata', type=inputs.boolean, help='Whether to keep the data or not', default=False, nullable=True) parser_post = ns.parser() parser_post.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True) + parser_post.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True) parser_get = ns.parser() parser_get.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True) + parser_get.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True) @api.disabled_on_demo() @api.acl_admin_or_moderator_required(message=_('Sorry, you don\'t have rights to access the setting panel')) @@ -528,7 +632,8 @@ class ClientSettings(Resource): args = self.parser_post.parse_args() template = args.get('template', False) - noti = bui.client.store_conf_cli(request.form, client, conf, template, server) + statictemplate = args.get('statictemplate', False) + noti = bui.client.store_conf_cli(request.form, client, conf, template, statictemplate, server) # clear cache cache.clear() # clear client-side cache through the _extra META variable @@ -559,8 +664,9 @@ class ClientSettings(Resource): pass args = self.parser_get.parse_args() template = args.get('template', False) + statictemplate = args.get('statictemplate', False) parser = bui.client.get_parser(agent=server) - res = parser.read_client_conf(client, conf, template) + res = parser.read_client_conf(client, conf, template, statictemplate) refresh() # Translate the doc and placeholder API side cache_keys = { @@ -627,6 +733,7 @@ class ClientSettings(Resource): revoke = args.get('revoke', False) keepconf = args.get('keepconf', False) template = args.get('template', False) + statictemplate = args.get('statictemplate', False) delete = args.get('delete', False) if not keepconf: @@ -647,8 +754,9 @@ class ClientSettings(Resource): bui.audit.logger.info( f'deleted client configuration {client}, delete certificate: {delcert}, ' f'revoke certificate: {revoke}, keep a backup of the configuration: ' - f'{keepconf}, delete data: {delete}, is template: {template}', server=server) - return parser.remove_client(client, keepconf, delcert, revoke, template, delete), 200 + f'{keepconf}, delete data: {delete}, is template: {template} ' + f'is static template: {statictemplate}', server=server) + return parser.remove_client(client, keepconf, delcert, revoke, template, statictemplate, delete), 200 @api.disabled_on_demo() @api.acl_admin_or_moderator_required(message=_('Sorry, you don\'t have rights to access the setting panel')) @@ -677,6 +785,7 @@ class ClientSettings(Resource): keepcert = args.get('keepcert', False) keepdata = args.get('keepdata', False) template = args.get('template', False) + statictemplate = args.get('statictemplate', False) # clear the cache when we remove a client cache.clear() @@ -695,8 +804,8 @@ class ClientSettings(Resource): bui.audit.logger.info( f'renaming client configuration {client} to {newname}, ' f'keep data: {keepdata}, keep certificate: {keepcert}, ' - f'is template: {template}', server=server) - return parser.rename_client(client, newname, template, keepcert, keepdata), 200 + f'is template: {template}, is static template: {statictemplate}', server=server) + return parser.rename_client(client, newname, template, statictemplate, keepcert, keepdata), 200 @ns.route('/path-expander', diff --git a/burpui/misc/backend/burp1.py b/burpui/misc/backend/burp1.py index e0e88106..a7a75f53 100644 --- a/burpui/misc/backend/burp1.py +++ b/burpui/misc/backend/burp1.py @@ -1013,7 +1013,8 @@ class Burp(BUIbackend): return [] return self.parser.read_server_conf(conf) - def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None): + def store_conf_cli(self, data, client=None, conf=None, template=False, + statictemplate=False, content='', agent=None): """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`""" if not self.parser: return [] @@ -1021,7 +1022,7 @@ class Burp(BUIbackend): conf = unquote(conf) except: pass - return self.parser.store_client_conf(data, client, conf, template) + return self.parser.store_client_conf(data, client, conf, template, statictemplate, content) def store_conf_srv(self, data, conf=None, agent=None): """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`""" diff --git a/burpui/misc/backend/interface.py b/burpui/misc/backend/interface.py index 17ada2a6..c513a94b 100644 --- a/burpui/misc/backend/interface.py +++ b/burpui/misc/backend/interface.py @@ -844,7 +844,8 @@ class BUIbackend(object, metaclass=ABCMeta): raise NotImplementedError("Sorry, the current Backend does not implement this method!") # pragma: no cover @abstractmethod - def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None): + def store_conf_cli(self, data, client=None, conf=None, template=False, + statictemplate=False, content='', agent=None): """The :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli` function works the same way as the :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv` function diff --git a/burpui/misc/backend/multi.py b/burpui/misc/backend/multi.py index d6097917..9cc8ad14 100644 --- a/burpui/misc/backend/multi.py +++ b/burpui/misc/backend/multi.py @@ -523,7 +523,8 @@ class NClient(BUIbackend): """ @implement - def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None): + def store_conf_cli(self, data, client=None, conf=None, template=False, + statictemplate=False, content='', agent=None): """See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`""" # serialize data as it is a nested dict import hmac @@ -539,7 +540,14 @@ class NClient(BUIbackend): data = ImmutableMultiDict(data.to_dict(False)) key = '{}{}'.format(self.password, 'store_conf_cli') key = to_bytes(key) - pickles = to_unicode(b64encode(pickle.dumps({'data': data, 'conf': conf, 'client': client, 'template': template}, 2))) + pickles = to_unicode( + b64encode( + pickle.dumps( + {'data': data, 'conf': conf, 'client': client, 'template': template, + 'statictemplate': statictemplate, 'content': content}, 2 + ) + ) + ) bytes_pickles = to_bytes(pickles) digest = to_unicode(hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest()) data = {'func': 'store_conf_cli', 'args': pickles, 'pickled': True, 'digest': digest} diff --git a/burpui/misc/parser/burp1.py b/burpui/misc/parser/burp1.py index 4f1c229e..1c638fc5 100644 --- a/burpui/misc/parser/burp1.py +++ b/burpui/misc/parser/burp1.py @@ -39,12 +39,17 @@ class Parser(Doc): self._client_conf = {} self._clients_conf = {} self._templates_conf = {} + self._static_templates_conf = {} self.clientconfdir = None self.clientconfdir_mtime = None self.templates = [] self.templates_dir = '.buitemplates' self.templates_path = None self.templates_mtime = None + self.static_templates = [] + self.static_templates_dir = '.buistatictemplates' + self.static_templates_path = None + self.static_templates_mtime = None self.filescache = {} self._configs = {} self.root = None @@ -102,6 +107,7 @@ class Parser(Doc): if purge: self._cleanup() self._list_templates(True) + self._list_static_templates(True) self._list_clients(True) def _load_conf_srv(self): @@ -115,6 +121,12 @@ class Parser(Doc): os.makedirs(self.templates_path, 0o755) except OSError as exp: self.logger.warning(str(exp)) + self.static_templates_path = os.path.join(self.clientconfdir, self.static_templates_dir) + if not os.path.exists(self.static_templates_path): + try: + os.makedirs(self.static_templates_path, 0o755) + except OSError as exp: + self.logger.warning(str(exp)) def _load_conf_cli(self): """Load the client configuration file""" @@ -156,6 +168,22 @@ class Parser(Doc): conf.parse() self._templates_conf[template['name']] = conf + def _load_conf_static_templates(self, name=None, in_path=None): + """Load all static templates configuration""" + if name: + templates = [{'name': name, 'value': in_path}] + else: + templates = self._list_static_templates(True) + + for template in templates: + conf = self.server_conf.clone() + path = os.path.join(self.static_templates_path, template['name']) + if template['name'] not in self._static_templates_conf: + conf.add_file(path) + conf.set_default(path) + conf.parse() + self._static_templates_conf[template['name']] = conf + def _load_all_conf(self): """Load all configurations""" self._cleanup() @@ -174,6 +202,11 @@ class Parser(Doc): self._load_conf_templates(name, path) return self._templates_conf[name] + def _new_static_template_conf(self, name, path): + """Create new static template conf""" + self._load_conf_static_templates(name, path) + return self._static_templates_conf[name] + def _clientconfdir_changed(self): """Detect changes in clientconfdir""" if not self.clientconfdir: @@ -196,6 +229,17 @@ class Parser(Doc): return True return False + def _static_templates_changed(self): + """Detect changes in static_templates_dir""" + if not self.static_templates_path: + return False + mtime = os.path.getmtime(self.static_templates_path) + changed = mtime != self.static_templates_mtime + if changed: + self.static_templates_mtime = mtime + return True + return False + def _get_client(self, name, path): """Return client conf and refresh it if necessary @@ -220,9 +264,25 @@ class Parser(Doc): self._load_conf_templates() if name not in self._templates_conf: return self._new_template_conf(name, path) - if self._templates_conf[name].changed: - self._templates_conf[name].parse() - return self._templates_conf[name] + template = self._templates_conf[name] + if template.changed: + template.parse() + return template + + def _get_static_template(self, name, path=None): + """Return static template conf and refresh it if necessary + + :rtype: Config + """ + if self._static_templates_changed() and name not in self._static_templates_conf: + self._static_templates_conf.clear() + self._load_conf_static_templates() + if name not in self._static_templates_conf: + return self._new_static_template_conf(name, path) + template = self._static_templates_conf[name] + if template.changed: + template.parse() + return template def _get_config(self, path, mode='cli'): """Return conf by it's path @@ -298,6 +358,35 @@ class Parser(Doc): self.templates_mtime = os.path.getmtime(self.templates_path) return res + def _list_static_templates(self, force=False): + res = [] + if not self.clientconfdir or not os.path.isdir(self.static_templates_path): + return res + + if self.static_templates and not force and \ + not self._clientconfdir_changed() and \ + not self._static_templates_changed(): + return self.static_templates + + for tpl in os.listdir(self.static_templates_path): + full_file = os.path.join(self.static_templates_path, tpl) + if os.path.isfile(full_file) and not tpl.startswith('.') and \ + not tpl.endswith('~'): + try: + with open(full_file) as template: + res.append({ + 'name': tpl, + 'value': os.path.join(self.static_templates_dir, tpl), + 'content': template.read() + }) + except OSError as exp: + self.logger.warning(str(exp)) + + self.static_templates = res + self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir) + self.static_templates_mtime = os.path.getmtime(self.static_templates_path) + return res + def _get_server_path(self, name=None, fil=''): """Returns the path of the 'server *fil*' file""" if not name: @@ -355,7 +444,7 @@ class Parser(Doc): return self.openssl_auth.check_client_revoked(client) def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False, - template=False, delete=False): + template=False, statictemplate=False, delete=False): """See :func:`burpui.misc.parser.interface.BUIparser.remove_client`""" res = [] revoked = False @@ -367,16 +456,20 @@ class Parser(Doc): if not keepconf: if template: path = os.path.join(self.templates_path, client) + elif statictemplate: + path = os.path.join(self.static_templates_path, client) else: path = os.path.join(self.clientconfdir, client) os.unlink(path) res.append([NOTIF_OK, "'{}' successfully removed".format(client)]) removed = True - if client in self._clients_conf and not template: + if client in self._clients_conf and not (template or statictemplate): del self._clients_conf[client] elif template and client in self._templates_conf: del self._templates_conf[client] + elif statictemplate and client in self._static_templates_conf: + del self._static_templates_conf[client] if path in self.filescache: del self.filescache[path] @@ -412,8 +505,8 @@ class Parser(Doc): return res - def rename_client(self, client=None, newname=None, template=False, keepcert=False, - keepdata=False): + def rename_client(self, client=None, newname=None, template=False, + statictemplate=False, keepcert=False, keepdata=False): """See :func:`burpui.misc.parser.interface.BUIparser.rename_client`""" res = [] if not client: @@ -424,6 +517,9 @@ class Parser(Doc): if template: path = os.path.join(self.templates_path, client) newpath = os.path.join(self.templates_path, newname) + elif statictemplate: + path = os.path.join(self.static_templates_path, client) + newpath = os.path.join(self.static_templates_path, newname) else: data = self._get_server_path(client) conf = self.clients_conf.get(client) @@ -436,16 +532,18 @@ class Parser(Doc): except OSError as exp: res.append([NOTIF_ERROR, str(exp)]) - if client in self._clients_conf and not template: + if client in self._clients_conf and not (template or statictemplate): del self._clients_conf[client] elif template and client in self._templates_conf: del self._templates_conf[client] + elif statictemplate and client in self._static_templates_conf: + del self._static_templates_conf[client] if path in self.filescache: del self.filescache[path] self._refresh_cache() - if template: + if template or statictemplate: return res if keepdata: @@ -485,7 +583,7 @@ class Parser(Doc): return res - def read_client_conf(self, client=None, conf=None, template=False): + def read_client_conf(self, client=None, conf=None, template=False, statictemplate=False): """ See :func:`burpui.misc.parser.interface.BUIparser.read_client_conf` """ @@ -510,6 +608,9 @@ class Parser(Doc): if template: mconf = os.path.join(self.templates_path, client) config = self._get_template(client, mconf) + elif statictemplate: + mconf = os.path.join(self.static_templates_path, client) + config = self._get_static_template(client, mconf) else: mconf = os.path.join(self.clientconfdir, client) config = self._get_client(client, mconf) @@ -599,7 +700,13 @@ class Parser(Doc): self.read_server_conf() return self._list_templates() - def store_client_conf(self, data, client=None, conf=None, template=False): + def list_static_templates(self): + """See :func:`burpui.misc.parser.interface.BUIparser.list_static_templates`""" + self.read_server_conf() + return self._list_static_templates() + + def store_client_conf(self, data, client=None, conf=None, template=False, + statictemplate=False, content=''): """ See :func:`burpui.misc.parser.interface.BUIparser.store_client_conf` """ @@ -608,21 +715,30 @@ class Parser(Doc): if not conf and not client: if template: return [[NOTIF_ERROR, 'Sorry, no template defined']] + elif statictemplate: + return [[NOTIF_ERROR, 'Sorry, no static template defined']] return [[NOTIF_ERROR, 'Sorry, no client defined']] elif client and not conf: if template: if not self.templates_path: return [[NOTIF_ERROR, 'Sorry, no template directory found']] conf = os.path.join(self.templates_path, client) + elif statictemplate: + if not self.static_templates_path: + return [[NOTIF_ERROR, 'Sorry, no static template directory found']] + conf = os.path.join(self.static_templates_path, client) else: conf = os.path.join(self.clientconfdir, client) - ret = self.store_conf(data, conf, client, mode='cli', template=template) + ret = self.store_conf(data, conf, client, mode='cli', template=template, + statictemplate=statictemplate, content=content) self._refresh_cache() # refresh client list return ret def store_conf(self, data, conf=None, client=None, mode='srv', - insecure=False, template=False): + insecure=False, template=False, statictemplate=False, + content=''): """See :func:`burpui.misc.parser.interface.BUIparser.store_conf`""" + ret = [] mconf = None if not conf: mconf = self.conf @@ -642,16 +758,42 @@ class Parser(Doc): ] ] + wrote_content = False + if content: + if os.path.exists(mconf): + ret.append([NOTIF_WARN, 'The file already exists, we won\'t override it']) + else: + try: + with open(mconf, 'w') as temp: + if not content.endswith('\n'): + content += '\n' + temp.write(content) + ret.append([NOTIF_OK, 'File successfully written from template']) + wrote_content = True + except IOError as exp: + ret.append([NOTIF_WARN, str(exp)]) + check = False if template: conffile = self._get_template(client, mconf).get_file(mconf) + elif statictemplate: + conffile = self._get_static_template(client, mconf).get_file(mconf) elif client: conffile = self._get_client(client, mconf).get_file(mconf) else: conffile = self.server_conf.get_file(mconf) check = True - ret = conffile.store_data(data, insecure) + if wrote_content and data: + for key in data.keys(): + tmp = data.getlist(key) + if len(tmp) == 1: + conffile[key] = tmp[0] + else: + conffile[key] = tmp + ret += conffile.store(insecure=insecure) + elif not wrote_content: + ret += conffile.store_data(data, insecure) if check: clientconfdir = conffile.get('clientconfdir') diff --git a/burpui/misc/parser/interface.py b/burpui/misc/parser/interface.py index 29d0b37d..c617a14e 100644 --- a/burpui/misc/parser/interface.py +++ b/burpui/misc/parser/interface.py @@ -133,7 +133,8 @@ class BUIparser(object, metaclass=ABCMeta): ) # pragma: no cover @abstractmethod - def store_client_conf(self, data, client=None, conf=None, template=False): + def store_client_conf(self, data, client=None, conf=None, template=False, + statictemplate=False, content=''): """:func:`burpui.misc.parser.interface.BUIparser.store_client_conf` is used by :func:`burpui.misc.backend.BUIbackend.store_conf_cli`. @@ -149,6 +150,12 @@ class BUIparser(object, metaclass=ABCMeta): :param template: Is this file a template :type template: bool + + :param statictemplate: Whether we remove a static template + :type statictemplate: bool + + :param content: What default content to put in the file + :type content: str """ raise NotImplementedError( "Sorry, the current Parser does not implement this method!" @@ -156,7 +163,7 @@ class BUIparser(object, metaclass=ABCMeta): @abstractmethod def store_conf(self, data, conf=None, client=None, mode='srv', - insecure=False, template=False): + insecure=False, template=False, statictemplate=False, content=''): """:func:`burpui.misc.parser.interface.BUIparser.store_conf` is used to store the configuration from the web-ui into the actual configuration files. @@ -181,6 +188,12 @@ class BUIparser(object, metaclass=ABCMeta): :param template: Is it a template :type template: bool + :param statictemplate: Whether we remove a static template + :type statictemplate: bool + + :param content: What default content to put in the file + :type content: str + :returns: A list of notifications to return to the UI (success or failure) @@ -248,6 +261,17 @@ class BUIparser(object, metaclass=ABCMeta): "Sorry, the current Parser does not implement this method!" ) # pragma: no cover + @abstractmethod + def list_static_templates(self): + """:func:`burpui.misc.parser.interface.BUIparser.list_static_templates` is used + to retrieve a list of static templates with their absolute paths. + + :returns: A list of templates + """ + raise NotImplementedError( + "Sorry, the current Parser does not implement this method!" + ) # pragma: no cover + @abstractmethod def is_client_revoked(self, client=None): """:func:`burpui.misc.parser.interface.BUIparser.is_client_revoked` is @@ -264,7 +288,7 @@ class BUIparser(object, metaclass=ABCMeta): @abstractmethod def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False, - template=False, delete=False): + template=False, statictemplate=False, delete=False): """:func:`burpui.misc.parser.interface.BUIparser.remove_client` is used to delete a client from burp's configuration. @@ -283,6 +307,9 @@ class BUIparser(object, metaclass=ABCMeta): :param template: Whether we remove a template :type template: bool + :param statictemplate: Whether we remove a static template + :type statictemplate: bool + :param delete: Whether to remove data as well :type delete: bool @@ -295,8 +322,8 @@ class BUIparser(object, metaclass=ABCMeta): ) # pragma: no cover @abstractmethod - def rename_client(self, client=None, newname=None, template=False, keepcert=False, - keepdata=False): + def rename_client(self, client=None, newname=None, template=False, + statictemplate=False, keepcert=False, keepdata=False): """:func:`burpui.misc.parser.interface.BUIParser.rename_client` is used to rename a client. @@ -309,6 +336,9 @@ class BUIparser(object, metaclass=ABCMeta): :param template: Whether we remove a template :type template: bool + :param statictemplate: Whether we remove a static template + :type statictemplate: bool + :param keepcert: Whether to keep using the same certificate or not :type keepcert: bool @@ -324,7 +354,7 @@ class BUIparser(object, metaclass=ABCMeta): ) # pragma: no cover @abstractmethod - def read_client_conf(self, client=None, conf=None, template=False): + def read_client_conf(self, client=None, conf=None, template=False, statictemplate=False): """:func:`burpui.misc.parser.interface.BUIparser.read_client_conf` is called by :func:`burpui.misc.backend.interface.BUIbackend.read_conf_cli` in order to parse the burp-clients configuration files. diff --git a/burpui/routes.py b/burpui/routes.py index c9c4ab5e..8b15457e 100644 --- a/burpui/routes.py +++ b/burpui/routes.py @@ -254,11 +254,13 @@ def cli_settings(server=None, client=None, conf=None): client = client or request.args.get('client') server = server or request.args.get('serverName') template = request.args.get('template') or False + statictemplate = request.args.get('statictemplate') or False return render_template( 'settings.html', settings=True, client_mode=True, template=template, + statictemplate=statictemplate, client=client, server=server, conf=conf, diff --git a/burpui/templates/js/settings.js b/burpui/templates/js/settings.js index a1d439a1..61d693b6 100644 --- a/burpui/templates/js/settings.js +++ b/burpui/templates/js/settings.js @@ -187,6 +187,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp {% if client -%} {% if template -%} $http.get('{{ url_for("api.client_settings", client=client, conf=conf, template=True, server=server) }}', { headers: { 'X-From-UI': true } }) + {% elif statictemplate -%} + $http.get('{{ url_for("api.client_settings", client=client, conf=conf, statictemplate=True, server=server) }}', { headers: { 'X-From-UI': true } }) {% else -%} $http.get('{{ url_for("api.client_settings", client=client, conf=conf, server=server) }}', { headers: { 'X-From-UI': true } }) {% endif -%} @@ -603,6 +605,25 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp } ); }; + $scope.getStaticTemplatesList = function() { + api = '{{ url_for("api.static_templates_list", server=server) }}'; + $http.get( + api, + { + headers: { 'X-From-UI': true }, + } + ).then( + function(response) { + var data = response.data; + $scope.raw.static_templates = data.result; + $scope.raw.static_templates.splice(0, 0, {'name': '{{ _("None") }}'}); + $scope.all.static_templates = {}; + _(data.result).forEach(function(r) { + $scope.all.static_templates[r.name] = {'value': r.value, 'variables': r.variables}; + }); + } + ); + }; $scope.deleteFile = function() { /* UX tweak: disable the submit button + change text */ submit = $('#btn-remove-file'); @@ -650,6 +671,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp type: 'PUT', {% if template -%} data: {template: true, newname: $('#newname').val() } + {% elif statictemplate -%} + data: {statictemplate: true, newname: $('#newname').val() } {% else -%} data: { newname: $('#newname').val(), keepcert: $('#keepcert').is(':checked'), keepdata: $('#keepdata').is(':checked') } {% endif -%} @@ -688,6 +711,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp type: 'DELETE', {% if template -%} data: { template: true } + {% elif statictemplate -%} + data: {statictemplate: true } {% else -%} data: { delcert: $('#delcert').is(':checked'), revoke: $('#revoke').is(':checked'), keepconf: $('#keepconf').is(':checked'), delete: $('#deldata').is(':checked') } {% endif -%} @@ -739,11 +764,24 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp e.preventDefault(); var form = $(e.target); var templates = form.find('input[name="templates"]'); - submit = form.find('button[type="submit"]'); - sav = submit.html(); + var variables = form.find('input[name="variables"]'); + var submit = form.find('button[type="submit"]'); + var disabled = []; + var sav = submit.html(); if ($scope.newclient.templates) { templates.val($scope.newclient.templates.join(',')); } + if ($scope.newclient.statictemplate && $scope.newclient.statictemplate != 'None') { + temp = {}; + _($('.static-variables').find('input')).forEach(function(raw) { + var input = $(raw); + console.log(input); + temp[input.attr('name')] = input.val(); + input.attr('disabled', true); + disabled.push(input); + }); + variables.val(JSON.stringify(temp)); + } submit.html(' {{ _("Creating...") }}'); submit.attr('disabled', true); $.ajax({ @@ -766,6 +804,9 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp .always(function() { submit.attr('disabled', false); submit.html(sav); + _(disabled).forEach(function(input) { + input.attr('disabled', false); + }); }); }; $scope.createTemplate = function(e) { @@ -798,6 +839,36 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp submit.html(sav); }); }; + $scope.createStaticTemplate = function(e) { + /* we disable the 'real' form submission */ + e.preventDefault(); + var form = $(e.target); + submit = form.find('button[type="submit"]'); + sav = submit.html(); + submit.html(' {{ _("Creating...") }}'); + submit.attr('disabled', true); + $.ajax({ + url: form.attr('action'), + type: 'PUT', + data: form.serialize() + }) + .fail(buiFail) + .done(function(data) { + /* The server answered correctly but some errors may have occurred server + * side so we display them */ + if (data.notif) { + notif(data.notif[0][0], data.notif[0][1]); + if (data.notif[0][0] == NOTIF_SUCCESS) { + $scope.getStaticTemplatesList(); + notif(data.notif[1][0], data.notif[1][1], 20000); + } + } + }) + .always(function() { + submit.attr('disabled', false); + submit.html(sav); + }); + }; $scope.isNumber = function(key) { return $scope.advanced && $scope.advanced[key] === 'integer'; }; @@ -823,10 +894,12 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp $scope.loadConfig(); $scope.getClientsList(); $scope.getTemplatesList(); + $scope.getStaticTemplatesList(); }]); {{ macros.page_length('#table-list-clients') }} {{ macros.page_length('#table-list-templates') }} +{{ macros.page_length('#table-list-static-templates') }} $(document).ready(function () { $('#config-nav a').click(function (e) { diff --git a/burpui/templates/settings.html b/burpui/templates/settings.html index 4f6e24b7..1a4685ad 100644 --- a/burpui/templates/settings.html +++ b/burpui/templates/settings.html @@ -18,6 +18,8 @@ {% else -%} {% if template -%}
  • {{ _('Template %(name)s on %(server)s', name=client, server=server) }}
  • + {% elif statictemplate -%} +
  • {{ _('Static template %(name)s on %(server)s', name=client, server=server) }}
  • {% else -%}
  • {{ _('%(client)s on %(server)s', client=client, server=server) }}
  • {% endif -%} @@ -40,6 +42,8 @@ {% else -%} {% if template -%}
  • {{ _('Template %(name)s', name=client) }}
  • + {% elif statictemplate -%} +
  • {{ _('Static template %(name)s', name=client) }}
  • {% else -%}
  • {{ client }}
  • {% endif -%} @@ -64,6 +68,7 @@ {% endif -%}
  • +
  • {% if (not is_moderator and is_admin) or client_mode -%} @@ -80,6 +85,8 @@ {% if client -%} {% if template -%}
    + {% elif statictemplate -%} + {% else -%} {% endif -%} @@ -486,7 +493,32 @@
    +
    + + + {% raw %} + {{ $select.selected.name }} + {% endraw %} + + +
    +
    +
    +
    +
    +
    + {% raw -%} + +
    + +
    +
    +
    + + @@ -525,6 +557,40 @@ +
    +
    +
    + + + + + + + + + {% raw -%} + + + {% endraw -%} + + + +
    {{ _('Name') }}{{ _('Path') }}
    {{ template.name }}{{ template.value }}
    +
    +
    +
    +
    + {{ _('Create new static template') }} +
    + + + + +
    +
    +
    +
    +
    {% endblock %} diff --git a/docs/upgrading.rst b/docs/upgrading.rst index bad3b15e..d1af9581 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -68,6 +68,14 @@ v0.7.0 from any rule with the ``exclude`` keyword. See the `BASIC ACL `__ documentation for details. +- **New** - You can now create *static templates* which support `jinja2 + variables `_ + format. These templates are applied only *once* at the creation of a new + client if you choose to use them. Also note there are two default variables: + ``{{client}}`` and ``{{agent}}`` injected while rendering them which contain + respectively the name of the *client* being created and the name of the + *agent* you are working on. + v0.6.0 ------