diff --git a/README.rst b/README.rst index 5951bff8..04315b00 100644 --- a/README.rst +++ b/README.rst @@ -872,9 +872,7 @@ Syntribos allows for templates to read in variables from a user-specified meta variable file. These files contain JSON objects that define variables to be used in one or more request templates. -The file must be named `meta.json` and must be placed in the same -directory as the template files that reference it. Meta variable files take -the form: +The file must be named `meta.json`, and they take the form: :: { @@ -884,11 +882,13 @@ the form: "user_name": { "type": config, "val": "user.username" + "fuzz_types": ["ascii"] }, "user_token": { "type": "function", "val": "syntribos.extensions.identity:get_scoped_token_v3", - "args": ["user"] + "args": ["user"], + "fuzz": false } } @@ -899,8 +899,6 @@ variables is as follows: POST /user HTTP/1.1 X-Auth-Token: |user_token| - Accept: */* - Content-type: application/json { "user": { @@ -909,6 +907,53 @@ variables is as follows: } } +Note: Meta-variable usage in templates should take the form `|user_name|`, not +`user_|name|` or `|user|_|name|`. This is to avoid ambiguous behavior when the +value is fuzzed. + +Meta Variable Attributes +------------------------ +* val - All meta variable objects must define a value, which can be of any json + DataType. Unlike the other attributes, this attribute is not optional. +* type - Defining a type instructs syntribos to interpret the variable in a + certain way. Any variables without a type defined will be read in directly + from the value. The following types are allowed: + + * config - syntribos reads the config value specified by the "val" + attribute and returns that value. + * function - syntribos calls the function named in the "val" attribute + with any arguments given in the optional "args" attribute, and returns the + value from calling the function. This value is cached, and will be returned + on subsequent calls. + * generator - Works the same way as the function type, but its results are + not cached and the function will be called every time. + +* args - A list of function arguments (if any) which can be defined here if the + variable is a generator or a function +* fuzz - A boolean value that, if set to false, instructs syntribos to + ignore this variable for any fuzz tests +* fuzz_types - A list of strings which instructs syntribos to only use certain + fuzz strings when fuzzing this variable. More than one fuzz type can be + defined. The following fuzz types are allowed: + + * ascii - strings that can be encoded as ascii + * url - strings that contain only url safe characters + +* min_length/max_length - An integer that instructs syntribos to only use fuzz + strings that meet certain length requirements + +Inheritence +----------- + +Meta variable files inherit based on the directory it's in. That is, if you +have `foo/meta.json` and `foo/bar/meta.json`, templates in `foo/bar/` will take +their meta variable values from `foo/bar/meta.json`, but they can also +reference meta variables that are defined only in `foo/meta.json`. This also +means that templates in `foo/baz/` cannot reference variables defined only in +`foo/bar/meta.json`. + +Each directory can have no more than one file named `meta.json`. + Running a specific test ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/test-anatomy.rst b/doc/source/test-anatomy.rst index 078b3cc8..31ff7ab0 100644 --- a/doc/source/test-anatomy.rst +++ b/doc/source/test-anatomy.rst @@ -152,9 +152,7 @@ Syntribos allows for templates to read in variables from a user-specified meta variable file. These files contain JSON objects that define variables to be used in one or more request templates. -The file must be named `meta.json` and must be placed in the same -directory as the template files that reference it. Meta variable files take -the form: +The file must be named `meta.json`, and they take the form: :: { @@ -164,11 +162,13 @@ the form: "user_name": { "type": config, "val": "user.username" + "fuzz_types": ["ascii"] }, "user_token": { "type": "function", "val": "syntribos.extensions.identity:get_scoped_token_v3", - "args": ["user"] + "args": ["user"], + "fuzz": false } } @@ -179,8 +179,6 @@ variables is as follows: POST /user HTTP/1.1 X-Auth-Token: |user_token| - Accept: */* - Content-type: application/json { "user": { @@ -189,6 +187,53 @@ variables is as follows: } } +Note: Meta-variable usage in templates should take the form `|user_name|`, not +`user_|name|` or `|user|_|name|`. This is to avoid ambiguous behavior when the +value is fuzzed. + +Meta Variable Attributes +------------------------ +* val - All meta variable objects must define a value, which can be of any json + DataType. Unlike the other attributes, this attribute is not optional. +* type - Defining a type instructs syntribos to interpret the variable in a + certain way. Any variables without a type defined will be read in directly + from the value. The following types are allowed: + + * config - syntribos reads the config value specified by the "val" + attribute and returns that value. + * function - syntribos calls the function named in the "val" attribute + with any arguments given in the optional "args" attribute, and returns the + value from calling the function. This value is cached, and will be returned + on subsequent calls. + * generator - Works the same way as the function type, but its results are + not cached and the function will be called every time. + +* args - A list of function arguments (if any) which can be defined here if the + variable is a generator or a function +* fuzz - A boolean value that, if set to false, instructs syntribos to + ignore this variable for any fuzz tests +* fuzz_types - A list of strings which instructs syntribos to only use certain + fuzz strings when fuzzing this variable. More than one fuzz type can be + defined. The following fuzz types are allowed: + + * ascii - strings that can be encoded as ascii + * url - strings that contain only url safe characters + +* min_length/max_length - An integer that instructs syntribos to only use fuzz + strings that meet certain length requirements + +Inheritence +----------- + +Meta variable files inherit based on the directory it's in. That is, if you +have `foo/meta.json` and `foo/bar/meta.json`, templates in `foo/bar/` will take +their meta variable values from `foo/bar/meta.json`, but they can also +reference meta variables that are defined only in `foo/meta.json`. This also +means that templates in `foo/baz/` cannot reference variables defined only in +`foo/bar/meta.json`. + +Each directory can have no more than one file named `meta.json`. + Running a specific test ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/syntribos/clients/http/parser.py b/syntribos/clients/http/parser.py index 4204d9d9..3ad27e14 100644 --- a/syntribos/clients/http/parser.py +++ b/syntribos/clients/http/parser.py @@ -63,7 +63,7 @@ class RequestCreator(object): break if lines[index] != "": index = index + 1 - method, url, params, _ = cls._parse_url_line(lines[0], endpoint) + method, url, params, version = cls._parse_url_line(lines[0], endpoint) headers = cls._parse_headers(lines[1:index]) data = cls._parse_data(lines[index + 1:]) return RequestObject( @@ -80,9 +80,9 @@ class RequestCreator(object): object read in from meta.json """ if var not in cls.meta_vars: - print("Expected to find {0} in meta.json, but didn't. " - "Check your templates".format(var)) - return + msg = _("Expected to find %s in meta.json, but didn't. " + "Check your templates") % var + raise TemplateParseException(msg) var_dict = cls.meta_vars[var] if "type" in var_dict: var_dict["var_type"] = var_dict.pop("type") @@ -116,17 +116,18 @@ class RequestCreator(object): try: return reduce(getattr, var_obj.val.split("."), CONF) except AttributeError: - print(_( - "Meta json file contains reference to the config option " - "%s, which does not appear to exist.") % var_obj.val) + msg = _("Meta json file contains reference to the config " + "option %s, which does not appear to" + "exist.") % var_obj.val + raise TemplateParseException(msg) + elif var_obj.var_type == 'function': if var_obj.function_return_value: return var_obj.function_return_value if not var_obj.val: - print(_( - "The type of variable is function, but there is no " - "reference to the function.")) - return + msg = _("The type of variable %s is function, but there is no " + "reference to the function.") % var_obj.name + raise TemplateParseException(msg) else: var_obj.function_return_value = cls.call_one_external_function( var_obj.val, var_obj.args) @@ -134,10 +135,10 @@ class RequestCreator(object): elif var_obj.var_type == 'generator': if not var_obj.val: - print(_( - "The type of variable {0} is generator, but there is no " - "reference to the function.")) - return + msg = _("The type of variable %s is generator, but there is no" + " reference to the function.") % var_obj.name + raise TemplateParseException(msg) + return cls.call_one_external_function(var_obj.val, var_obj.args) else: return str(var_obj.val) @@ -147,17 +148,24 @@ class RequestCreator(object): """Recursively evaluates all meta variables in a given dict.""" for (key, value) in dic.items(): # Keys dont get fuzzed, so can handle them here - new_key = key.strip("|%s" % string.whitespace) - if re.search(cls.METAVAR, key): - key_obj = cls._create_var_obj(new_key) - new_key = cls.replace_one_variable(key_obj) + match = re.search(cls.METAVAR, key) + if match: + replaced_key = match.group(0).strip("|") + key_obj = cls._create_var_obj(replaced_key) + replaced_key = cls.replace_one_variable(key_obj) + new_key = re.sub(cls.METAVAR, replaced_key, key) del dic[key] dic[new_key] = value # Vals are fuzzed so they need to be passed to datagen as an object if isinstance(value, six.string_types): - if re.search(cls.METAVAR, value): - value = value.strip("|%s" % string.whitespace) - val_obj = cls._create_var_obj(value) + match = re.search(cls.METAVAR, value) + if match: + var_str = match.group(0).strip("|") + if var_str != value.strip("|%s" % string.whitespace): + msg = _("Meta-variable references cannot come in the " + "middle of the value %s") % value + raise TemplateParseException(msg) + val_obj = cls._create_var_obj(var_str) if key in dic: dic[key] = val_obj elif new_key in dic: @@ -214,7 +222,7 @@ class RequestCreator(object): url = url[0] url = urlparse.urljoin(endpoint, url) if method not in valid_methods: - raise ValueError("Invalid HTTP method: {0}".format(method)) + raise ValueError(_("Invalid HTTP method: %s") % method) return (method, cls._replace_str_variables(url), cls._replace_dict_variables(params), version) @@ -253,12 +261,16 @@ class RequestCreator(object): return cls._replace_dict_variables(data) else: return cls._replace_str_variables(data) - except Exception: + except TemplateParseException: + raise + except (TypeError, ValueError): try: data = ElementTree.fromstring(data) except Exception: if not re.match(postdat_regex, data): raise TypeError(_("Unknown data format")) + except Exception: + raise return data @classmethod @@ -301,19 +313,44 @@ class RequestCreator(object): if not match: match = re.search(cls.FUNC_WITH_ARGS, string) func_string_has_args = True - if not match: - print(_("The reference to the function %s failed to parse " - "correctly") % string) - return - dot_path = match.group(1) - func_name = match.group(2) - mod = importlib.import_module(dot_path) - func = getattr(mod, func_name) - if func_string_has_args and not args: - arg_list = match.group(3) - args = json.loads(arg_list) - val = func(*args) + if match: + try: + dot_path = match.group(1) + func_name = match.group(2) + mod = importlib.import_module(dot_path) + func = getattr(mod, func_name) + + if func_string_has_args and not args: + arg_list = match.group(3) + args = json.loads(arg_list) + + val = func(*args) + except Exception: + msg = _("The reference to the function %s failed to parse " + "correctly, please check the documentation to ensure " + "your function import string adheres to the proper " + "format") % string + raise TemplateParseException(msg) + + else: + try: + func_lst = string.split(":") + if len(func_lst) == 2: + args = func_lst[1] + func_str = func_lst[0] + dot_path = ".".join(func_str.split(".")[:-1]) + func_name = func_str.split(".")[-1] + mod = importlib.import_module(dot_path) + func = getattr(mod, func_name) + val = func(*args) + except Exception: + msg = _("The reference to the function %s failed to parse " + "correctly, please check the documentation to ensure " + "your function import string adheres to the proper " + "format") % string + raise TemplateParseException(msg) + if isinstance(val, types.GeneratorType): return str(six.next(val)) else: @@ -328,10 +365,11 @@ class VariableObject(object): fuzz_types=[], min_length=0, max_length=sys.maxsize, url_encode=False, **kwargs): if var_type and var_type.lower() not in self.VAR_TYPES: - print(_( - "The variable %(name)s has a type of %(var)s which" - " syntribos does not" - " recognize") % {'name': name, 'var': var_type}) + msg = _("The meta variable %(name)s has a type of %(var)s which " + "syntribos does not" + "recognize") % {'name': name, 'var': var_type} + raise TemplateParseException(msg) + self.name = name self.var_type = var_type.lower() self.val = val @@ -347,6 +385,10 @@ class VariableObject(object): return str(vars(self)) +class TemplateParseException(Exception): + pass + + class RequestHelperMixin(object): """Class that helps with fuzzing requests.""" diff --git a/syntribos/runner.py b/syntribos/runner.py index 3114527d..f9e4153a 100644 --- a/syntribos/runner.py +++ b/syntribos/runner.py @@ -159,6 +159,26 @@ class Runner(object): else: cls.output = sys.stdout + @classmethod + def get_meta_vars(cls, file_path): + """Creates the appropriate meta_var dict for the given file path + + Meta variables are inherited according to directory. This function + builds a meta variable dict from the top down. + + :param file_path: the path of the current template + :returns: `dict` of meta variables + """ + path_segments = [""] + os.path.dirname(file_path).split(os.sep) + meta_vars = {} + current_path = "" + for seg in path_segments: + current_path = os.path.join(current_path, seg) + if current_path in cls.meta_dir_dict: + for k, v in cls.meta_dir_dict[current_path].items(): + meta_vars[k] = v + return meta_vars + @classmethod def run(cls): """Method sets up logger and decides on Syntribos control flow @@ -228,21 +248,21 @@ class Runner(object): exit(1) print(_("\nPress Ctrl-C to pause or exit...\n")) - # TODO(mdong): Make this handle inheritence and all that. For now, just - # pass the first meta file it sees to the parser. Also, find a better - # way to pass meta_vars meta_vars = None templates_dir = list(templates_dir) - for file_path, req_str in templates_dir: - if "meta.json" not in file_path: - continue - else: - meta_vars = json.loads(req_str) - break + cls.meta_dir_dict = {} + for file_path, file_content in templates_dir: + if os.path.basename(file_path) == "meta.json": + meta_path = os.path.dirname(file_path) + try: + cls.meta_dir_dict[meta_path] = json.loads(file_content) + except Exception: + print("Unable to parse %s, skipping..." % file_path) for file_path, req_str in templates_dir: if "meta.json" in file_path: continue + meta_vars = cls.get_meta_vars(file_path) LOG = cls.get_logger(file_path) CONF.log_opt_values(LOG, logging.DEBUG) if not file_path.endswith(".template"): @@ -294,10 +314,10 @@ class Runner(object): """ for k, test_class in list_of_tests: # noqa try: - print("\nParsing template file...") + print("\nParsing template file...\n") test_class.create_init_request(file_path, req_str, meta_vars) except Exception as e: - print("Error in parsing template:\n \t{0}\n".format( + print("\nError in parsing template:\n \t{0}\n".format( traceback.format_exc())) LOG.error(_LE("Error in parsing template:")) output["failures"].append({ @@ -305,7 +325,7 @@ class Runner(object): "error": e.__str__() }) else: - print(_("Request sucessfully generated!\n")) + print(_("\nRequest sucessfully generated!\n")) output["successes"].append(file_path) test_cases = list(test_class.get_test_cases(file_path, req_str)) diff --git a/syntribos/tests/base.py b/syntribos/tests/base.py index e1e9bfb7..d91a7a99 100644 --- a/syntribos/tests/base.py +++ b/syntribos/tests/base.py @@ -246,8 +246,6 @@ class BaseTestCase(unittest.TestCase): confidence=confidence, description=description) - # Still associating request and response objects with issue in event of - # debug log issue.request = self.test_req issue.response = self.test_resp diff --git a/syntribos/tests/headers/xst.py b/syntribos/tests/headers/xst.py index 154afc42..3017c0e5 100644 --- a/syntribos/tests/headers/xst.py +++ b/syntribos/tests/headers/xst.py @@ -70,6 +70,4 @@ class XstHeader(base.BaseTestCase): confidence=syntribos.HIGH, description=(_("XST vulnerability found.\n" "Make sure that response to a " - "TRACE request is filtered." - ) - )) + "TRACE request is filtered.")))