From 014039fd097e46ca1ed5d8637c3936b9537555b2 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Fri, 13 Dec 2024 15:43:06 -0500 Subject: [PATCH 1/7] Check only root level banner messages for ASA configs --- netutils/config/parser.py | 70 ++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/netutils/config/parser.py b/netutils/config/parser.py index 5e4ddbb8..67450fda 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -195,7 +195,9 @@ def _remove_parents(self, line: str, current_spaces: int) -> t.Tuple[str, ...]: previous_parent = self._current_parents[-deindent_level] previous_indent = self.get_leading_space_count(previous_parent) except IndexError: - raise IndexError(f"\nValidate the first line does not begin with a space\n{line}\n") + raise IndexError( + f"\nValidate the first line does not begin with a space\n{line}\n" + ) parents = self._current_parents[:-deindent_level] or (self._current_parents[0],) return parents @@ -417,7 +419,9 @@ def find_children_w_parents( ] for cfg_line in self.build_config_relationship(): parents = cfg_line.parents[0] if cfg_line.parents else None - if parents in potential_parents and self._match_type_check(parents, parent_pattern, match_type): + if parents in potential_parents and self._match_type_check( + parents, parent_pattern, match_type + ): config.append(cfg_line.config_line) return config @@ -434,7 +438,11 @@ def config_lines_only(self) -> str: Returns: The non-space lines from ``config``. """ - config_lines = [line.rstrip() for line in self.config.splitlines() if line and not line.isspace()] + config_lines = [ + line.rstrip() + for line in self.config.splitlines() + if line and not line.isspace() + ] return "\n".join(config_lines) def build_config_relationship(self) -> t.List[ConfigLine]: @@ -512,7 +520,9 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: for line in self.generator_config: multiline_config.append(line) if line.lstrip() == delimiter: - multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) + multiline_entry = ConfigLine( + "\n".join(multiline_config), self._current_parents + ) self.config_lines.append(multiline_entry) self._current_parents = self._current_parents[:-1] return multiline_entry @@ -522,7 +532,9 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: class CiscoConfigParser(BaseSpaceConfigParser): """Cisco Implementation of ConfigParser Class.""" - regex_banner = re.compile(r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)") + regex_banner = re.compile( + r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)" + ) def __init__(self, config: str): """Create ConfigParser Object. @@ -584,7 +596,9 @@ def banner_end(self) -> str: def banner_end(self, banner_start_line: str) -> None: banner_parsed = self.regex_banner.match(banner_start_line) if not banner_parsed: - raise ValueError("There was an error parsing your banner, the end of the banner could not be found") + raise ValueError( + "There was an error parsing your banner, the end of the banner could not be found" + ) self._banner_end = banner_parsed.groupdict()["banner_delimiter"] @@ -853,7 +867,9 @@ def build_config_relationship(self) -> t.List[ConfigLine]: return self.config_lines - def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: str) -> t.Optional[ConfigLine]: + def _build_multiline_single_configuration_line( + self, delimiter: str, prev_line: str + ) -> t.Optional[ConfigLine]: r"""Concatenate Multiline strings between delimiter when newlines causes string to traverse multiple lines. Args: @@ -891,7 +907,9 @@ def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: for line in self.generator_config: multiline_config.append(line) if line.endswith(delimiter): - multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) + multiline_entry = ConfigLine( + "\n".join(multiline_config), self._current_parents + ) self.config_lines[-1] = multiline_entry self._current_parents = self._current_parents[:-1] return multiline_entry @@ -925,6 +943,22 @@ def __init__(self, config: str): self.same_line_children: t.Set[ConfigLine] = set() super(ASAConfigParser, self).__init__(config) + def is_banner_start(self, line: str) -> bool: + """Determine if the line starts a banner config. + + Args: + line: The current config line in iteration. + + Returns: + True if line starts banner, else False. + """ + for banner_start in self.banner_start: + if not line: + return False + if line.startswith(banner_start): + return True + return False + def _update_config_lines(self, config_line: str) -> None: """Add a ``ConfigLine`` object to ``self.config_lines``. @@ -1037,7 +1071,9 @@ def _parse_out_offending(self, config: str) -> str: # This will grab everything between quotes after the 'set buffer' sub-command. # Its explicitly looking for "\n to end the captured data. This is to support html # data that is supported in Fortinet config with double quotes within the html. - pattern = r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" + pattern = ( + r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" + ) return re.sub(pattern, r"\1 [\2]\n", config) @property @@ -1053,7 +1089,10 @@ def config_lines_only(self) -> str: config_lines = ( line.rstrip() for line in self.config.splitlines() - if line and not self.is_comment(line) and not line.isspace() and not self.is_end_next(line) + if line + and not self.is_comment(line) + and not line.isspace() + and not self.is_end_next(line) ) self._config = "\n".join(config_lines) return self._config @@ -1453,7 +1492,12 @@ class PaloAltoNetworksConfigParser(BaseSpaceConfigParser): def is_banner_end(self, line: str) -> bool: """Determine if end of banner.""" - if line.endswith('"') or line.startswith('";') or line.startswith("set") or line.endswith(self.banner_end): + if ( + line.endswith('"') + or line.startswith('";') + or line.startswith("set") + or line.endswith(self.banner_end) + ): return True return False @@ -1521,7 +1565,9 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to if line.endswith("{"): _needs_conversion = True if _needs_conversion: - converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string") + converted_config = paloalto_panos_brace_to_set( + cfg=self.config, cfg_type="string" + ) list_config = converted_config.splitlines() self.generator_config = (line for line in list_config) From f57ea3352ed9b08baa3080e153ef105fb02c3a1a Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Mon, 16 Dec 2024 09:23:11 -0500 Subject: [PATCH 2/7] black fixes --- netutils/config/parser.py | 54 +++++++++------------------------------ 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/netutils/config/parser.py b/netutils/config/parser.py index 67450fda..659b65c9 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -195,9 +195,7 @@ def _remove_parents(self, line: str, current_spaces: int) -> t.Tuple[str, ...]: previous_parent = self._current_parents[-deindent_level] previous_indent = self.get_leading_space_count(previous_parent) except IndexError: - raise IndexError( - f"\nValidate the first line does not begin with a space\n{line}\n" - ) + raise IndexError(f"\nValidate the first line does not begin with a space\n{line}\n") parents = self._current_parents[:-deindent_level] or (self._current_parents[0],) return parents @@ -419,9 +417,7 @@ def find_children_w_parents( ] for cfg_line in self.build_config_relationship(): parents = cfg_line.parents[0] if cfg_line.parents else None - if parents in potential_parents and self._match_type_check( - parents, parent_pattern, match_type - ): + if parents in potential_parents and self._match_type_check(parents, parent_pattern, match_type): config.append(cfg_line.config_line) return config @@ -438,11 +434,7 @@ def config_lines_only(self) -> str: Returns: The non-space lines from ``config``. """ - config_lines = [ - line.rstrip() - for line in self.config.splitlines() - if line and not line.isspace() - ] + config_lines = [line.rstrip() for line in self.config.splitlines() if line and not line.isspace()] return "\n".join(config_lines) def build_config_relationship(self) -> t.List[ConfigLine]: @@ -520,9 +512,7 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: for line in self.generator_config: multiline_config.append(line) if line.lstrip() == delimiter: - multiline_entry = ConfigLine( - "\n".join(multiline_config), self._current_parents - ) + multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) self.config_lines.append(multiline_entry) self._current_parents = self._current_parents[:-1] return multiline_entry @@ -532,9 +522,7 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: class CiscoConfigParser(BaseSpaceConfigParser): """Cisco Implementation of ConfigParser Class.""" - regex_banner = re.compile( - r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)" - ) + regex_banner = re.compile(r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)") def __init__(self, config: str): """Create ConfigParser Object. @@ -596,9 +584,7 @@ def banner_end(self) -> str: def banner_end(self, banner_start_line: str) -> None: banner_parsed = self.regex_banner.match(banner_start_line) if not banner_parsed: - raise ValueError( - "There was an error parsing your banner, the end of the banner could not be found" - ) + raise ValueError("There was an error parsing your banner, the end of the banner could not be found") self._banner_end = banner_parsed.groupdict()["banner_delimiter"] @@ -867,9 +853,7 @@ def build_config_relationship(self) -> t.List[ConfigLine]: return self.config_lines - def _build_multiline_single_configuration_line( - self, delimiter: str, prev_line: str - ) -> t.Optional[ConfigLine]: + def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: str) -> t.Optional[ConfigLine]: r"""Concatenate Multiline strings between delimiter when newlines causes string to traverse multiple lines. Args: @@ -907,9 +891,7 @@ def _build_multiline_single_configuration_line( for line in self.generator_config: multiline_config.append(line) if line.endswith(delimiter): - multiline_entry = ConfigLine( - "\n".join(multiline_config), self._current_parents - ) + multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) self.config_lines[-1] = multiline_entry self._current_parents = self._current_parents[:-1] return multiline_entry @@ -1071,9 +1053,7 @@ def _parse_out_offending(self, config: str) -> str: # This will grab everything between quotes after the 'set buffer' sub-command. # Its explicitly looking for "\n to end the captured data. This is to support html # data that is supported in Fortinet config with double quotes within the html. - pattern = ( - r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" - ) + pattern = r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" return re.sub(pattern, r"\1 [\2]\n", config) @property @@ -1089,10 +1069,7 @@ def config_lines_only(self) -> str: config_lines = ( line.rstrip() for line in self.config.splitlines() - if line - and not self.is_comment(line) - and not line.isspace() - and not self.is_end_next(line) + if line and not self.is_comment(line) and not line.isspace() and not self.is_end_next(line) ) self._config = "\n".join(config_lines) return self._config @@ -1492,12 +1469,7 @@ class PaloAltoNetworksConfigParser(BaseSpaceConfigParser): def is_banner_end(self, line: str) -> bool: """Determine if end of banner.""" - if ( - line.endswith('"') - or line.startswith('";') - or line.startswith("set") - or line.endswith(self.banner_end) - ): + if line.endswith('"') or line.startswith('";') or line.startswith("set") or line.endswith(self.banner_end): return True return False @@ -1565,9 +1537,7 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to if line.endswith("{"): _needs_conversion = True if _needs_conversion: - converted_config = paloalto_panos_brace_to_set( - cfg=self.config, cfg_type="string" - ) + converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string") list_config = converted_config.splitlines() self.generator_config = (line for line in list_config) From 3f6f6a715cb720ec8a748c08537108141b003210 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Thu, 19 Dec 2024 14:01:58 -0500 Subject: [PATCH 3/7] Adding asa nested test and black updates --- tests/unit/test_parser.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index c7ff747e..fc50ed15 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -4,7 +4,9 @@ import os import pytest + from netutils.config import compliance +from netutils.config.parser import ConfigLine MOCK_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mock", "config", "parser") TXT_FILE = "_sent.txt" @@ -79,3 +81,21 @@ def test_duplicate_line(): ) with pytest.raises(IndexError, match=r".*This error is likely from a duplicate line detected.*"): compliance.parser_map["cisco_ios"](logging).config_lines # pylint: disable=expression-not-assigned + + +def test_nested_banner(): + banner: str = ( + "group-policy Grs-POLICY attributes\n" " banner value This is an\n" " banner value example nested banner.\n" + ) + + generated_config_lines: list[ConfigLine] = compliance.parser_map["cisco_asa"]( + banner, + ).config_lines + + parent: str = "group-policy Grs-POLICY attributes" + mock_config_lines: list[ConfigLine] = [ + ConfigLine(config_line=parent, parents=()), + ConfigLine(config_line=" banner value This is an", parents=(parent,)), + ConfigLine(config_line=" banner value example nested banner.", parents=(parent,)), + ] + assert generated_config_lines == mock_config_lines From 41f588635fa1d6ef25a18e4191b160e297bce510 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Thu, 19 Dec 2024 15:09:15 -0500 Subject: [PATCH 4/7] updating ASAConfigParse banner_start list and updating tests to follow pattern --- netutils/config/parser.py | 55 ++++++++++++---- .../cisco_asa/asa_nested_banner_received.py | 8 +++ .../base/cisco_asa/asa_nested_banner_sent.txt | 3 + tests/unit/test_parser.py | 66 +++++++++++-------- 4 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py create mode 100644 tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_sent.txt diff --git a/netutils/config/parser.py b/netutils/config/parser.py index 659b65c9..6c53c1d6 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -195,7 +195,9 @@ def _remove_parents(self, line: str, current_spaces: int) -> t.Tuple[str, ...]: previous_parent = self._current_parents[-deindent_level] previous_indent = self.get_leading_space_count(previous_parent) except IndexError: - raise IndexError(f"\nValidate the first line does not begin with a space\n{line}\n") + raise IndexError( + f"\nValidate the first line does not begin with a space\n{line}\n" + ) parents = self._current_parents[:-deindent_level] or (self._current_parents[0],) return parents @@ -417,7 +419,9 @@ def find_children_w_parents( ] for cfg_line in self.build_config_relationship(): parents = cfg_line.parents[0] if cfg_line.parents else None - if parents in potential_parents and self._match_type_check(parents, parent_pattern, match_type): + if parents in potential_parents and self._match_type_check( + parents, parent_pattern, match_type + ): config.append(cfg_line.config_line) return config @@ -434,7 +438,11 @@ def config_lines_only(self) -> str: Returns: The non-space lines from ``config``. """ - config_lines = [line.rstrip() for line in self.config.splitlines() if line and not line.isspace()] + config_lines = [ + line.rstrip() + for line in self.config.splitlines() + if line and not line.isspace() + ] return "\n".join(config_lines) def build_config_relationship(self) -> t.List[ConfigLine]: @@ -512,7 +520,9 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: for line in self.generator_config: multiline_config.append(line) if line.lstrip() == delimiter: - multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) + multiline_entry = ConfigLine( + "\n".join(multiline_config), self._current_parents + ) self.config_lines.append(multiline_entry) self._current_parents = self._current_parents[:-1] return multiline_entry @@ -522,7 +532,9 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: class CiscoConfigParser(BaseSpaceConfigParser): """Cisco Implementation of ConfigParser Class.""" - regex_banner = re.compile(r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)") + regex_banner = re.compile( + r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)" + ) def __init__(self, config: str): """Create ConfigParser Object. @@ -584,7 +596,9 @@ def banner_end(self) -> str: def banner_end(self, banner_start_line: str) -> None: banner_parsed = self.regex_banner.match(banner_start_line) if not banner_parsed: - raise ValueError("There was an error parsing your banner, the end of the banner could not be found") + raise ValueError( + "There was an error parsing your banner, the end of the banner could not be found" + ) self._banner_end = banner_parsed.groupdict()["banner_delimiter"] @@ -853,7 +867,9 @@ def build_config_relationship(self) -> t.List[ConfigLine]: return self.config_lines - def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: str) -> t.Optional[ConfigLine]: + def _build_multiline_single_configuration_line( + self, delimiter: str, prev_line: str + ) -> t.Optional[ConfigLine]: r"""Concatenate Multiline strings between delimiter when newlines causes string to traverse multiple lines. Args: @@ -891,7 +907,9 @@ def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: for line in self.generator_config: multiline_config.append(line) if line.endswith(delimiter): - multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) + multiline_entry = ConfigLine( + "\n".join(multiline_config), self._current_parents + ) self.config_lines[-1] = multiline_entry self._current_parents = self._current_parents[:-1] return multiline_entry @@ -913,6 +931,7 @@ def banner_end(self) -> str: class ASAConfigParser(CiscoConfigParser): """Cisco ASA implementation of ConfigParser Class.""" + banner_start: t.List[str] = ["banner"] comment_chars: t.List[str] = ["!", ":"] def __init__(self, config: str): @@ -1053,7 +1072,9 @@ def _parse_out_offending(self, config: str) -> str: # This will grab everything between quotes after the 'set buffer' sub-command. # Its explicitly looking for "\n to end the captured data. This is to support html # data that is supported in Fortinet config with double quotes within the html. - pattern = r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" + pattern = ( + r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" + ) return re.sub(pattern, r"\1 [\2]\n", config) @property @@ -1069,7 +1090,10 @@ def config_lines_only(self) -> str: config_lines = ( line.rstrip() for line in self.config.splitlines() - if line and not self.is_comment(line) and not line.isspace() and not self.is_end_next(line) + if line + and not self.is_comment(line) + and not line.isspace() + and not self.is_end_next(line) ) self._config = "\n".join(config_lines) return self._config @@ -1469,7 +1493,12 @@ class PaloAltoNetworksConfigParser(BaseSpaceConfigParser): def is_banner_end(self, line: str) -> bool: """Determine if end of banner.""" - if line.endswith('"') or line.startswith('";') or line.startswith("set") or line.endswith(self.banner_end): + if ( + line.endswith('"') + or line.startswith('";') + or line.startswith("set") + or line.endswith(self.banner_end) + ): return True return False @@ -1537,7 +1566,9 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to if line.endswith("{"): _needs_conversion = True if _needs_conversion: - converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string") + converted_config = paloalto_panos_brace_to_set( + cfg=self.config, cfg_type="string" + ) list_config = converted_config.splitlines() self.generator_config = (line for line in list_config) diff --git a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py new file mode 100644 index 00000000..05763626 --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py @@ -0,0 +1,8 @@ +from netutils.config.parser import ConfigLine + +parent: str = "group-policy Grs-POLICY attributes" +data: list[ConfigLine] = [ + ConfigLine(config_line=parent, parents=()), + ConfigLine(config_line=" banner value This is an", parents=(parent,)), + ConfigLine(config_line=" banner value example nested banner", parents=(parent,)), +] diff --git a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_sent.txt b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_sent.txt new file mode 100644 index 00000000..ed8c8f09 --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_sent.txt @@ -0,0 +1,3 @@ +group-policy Grs-POLICY attributes + banner value This is an + banner value example nested banner \ No newline at end of file diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index fc50ed15..193558b6 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -6,9 +6,10 @@ import pytest from netutils.config import compliance -from netutils.config.parser import ConfigLine -MOCK_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mock", "config", "parser") +MOCK_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "mock", "config", "parser" +) TXT_FILE = "_sent.txt" base_parameters = [] @@ -19,7 +20,9 @@ base_parameters.append([_file, network_os]) for _file in glob.glob(f"{MOCK_DIR}/find_all_children/{network_os}/*{TXT_FILE}"): find_all_children_parameters.append([_file, network_os]) - for _file in glob.glob(f"{MOCK_DIR}/find_children_w_parents/{network_os}/*{TXT_FILE}"): + for _file in glob.glob( + f"{MOCK_DIR}/find_children_w_parents/{network_os}/*{TXT_FILE}" + ): find_children_w_parents_parameters.append([_file, network_os]) @@ -38,23 +41,32 @@ def test_find_all_children(_file, network_os, get_text_data, get_json_data): # truncate_file = os.path.join(MOCK_DIR, "find_all_children", _file[: -len(TXT_FILE)]) device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_all_children", _file)) - received_data = get_text_data(os.path.join(MOCK_DIR, "find_all_children", truncate_file + "_received.txt")) + received_data = get_text_data( + os.path.join(MOCK_DIR, "find_all_children", truncate_file + "_received.txt") + ) kwargs = get_json_data(truncate_file + "_args.json") os_parser = compliance.parser_map[network_os] assert "\n".join(os_parser(device_cfg).find_all_children(**kwargs)) == received_data @pytest.mark.parametrize("_file, network_os", find_children_w_parents_parameters) -def test_find_children_w_parents( - _file, network_os, get_text_data, get_json_data -): # pylint: disable=redefined-outer-name - truncate_file = os.path.join(MOCK_DIR, "find_children_w_parents", _file[: -len(TXT_FILE)]) +def test_find_children_w_parents(_file, network_os, get_text_data, get_json_data): # pylint: disable=redefined-outer-name + truncate_file = os.path.join( + MOCK_DIR, "find_children_w_parents", _file[: -len(TXT_FILE)] + ) device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_children_w_parents", _file)) - received_data = get_text_data(os.path.join(MOCK_DIR, "find_children_w_parents", truncate_file + "_received.txt")) + received_data = get_text_data( + os.path.join( + MOCK_DIR, "find_children_w_parents", truncate_file + "_received.txt" + ) + ) kwargs = get_json_data(truncate_file + "_args.json") os_parser = compliance.parser_map[network_os] - assert "\n".join(os_parser(device_cfg).find_children_w_parents(**kwargs)) == received_data + assert ( + "\n".join(os_parser(device_cfg).find_children_w_parents(**kwargs)) + == received_data + ) def test_incorrect_banner_ios(): @@ -79,23 +91,25 @@ def test_duplicate_line(): "snmp-server community <> RO SNMP_ACL_RO\n" "snmp-server community <> RW SNMP_ACL_RW\n" ) - with pytest.raises(IndexError, match=r".*This error is likely from a duplicate line detected.*"): + with pytest.raises( + IndexError, match=r".*This error is likely from a duplicate line detected.*" + ): compliance.parser_map["cisco_ios"](logging).config_lines # pylint: disable=expression-not-assigned -def test_nested_banner(): - banner: str = ( - "group-policy Grs-POLICY attributes\n" " banner value This is an\n" " banner value example nested banner.\n" - ) +# def test_nested_banner(): +# banner: str = ( +# "group-policy Grs-POLICY attributes\n" " banner value This is an\n" " banner value example nested banner.\n" +# ) + +# generated_config_lines: list[ConfigLine] = compliance.parser_map["cisco_asa"]( +# banner, +# ).config_lines - generated_config_lines: list[ConfigLine] = compliance.parser_map["cisco_asa"]( - banner, - ).config_lines - - parent: str = "group-policy Grs-POLICY attributes" - mock_config_lines: list[ConfigLine] = [ - ConfigLine(config_line=parent, parents=()), - ConfigLine(config_line=" banner value This is an", parents=(parent,)), - ConfigLine(config_line=" banner value example nested banner.", parents=(parent,)), - ] - assert generated_config_lines == mock_config_lines +# parent: str = "group-policy Grs-POLICY attributes" +# mock_config_lines: list[ConfigLine] = [ +# ConfigLine(config_line=parent, parents=()), +# ConfigLine(config_line=" banner value This is an", parents=(parent,)), +# ConfigLine(config_line=" banner value example nested banner.", parents=(parent,)), +# ] +# assert generated_config_lines == mock_config_lines From 1dfdb401cd0e6f293be65d6ee628e5f6965ec793 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Thu, 19 Dec 2024 15:19:02 -0500 Subject: [PATCH 5/7] black fixes --- netutils/config/parser.py | 54 +++++++++------------------------------ tests/unit/test_parser.py | 53 ++++++++------------------------------ 2 files changed, 22 insertions(+), 85 deletions(-) diff --git a/netutils/config/parser.py b/netutils/config/parser.py index 6c53c1d6..40d62025 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -195,9 +195,7 @@ def _remove_parents(self, line: str, current_spaces: int) -> t.Tuple[str, ...]: previous_parent = self._current_parents[-deindent_level] previous_indent = self.get_leading_space_count(previous_parent) except IndexError: - raise IndexError( - f"\nValidate the first line does not begin with a space\n{line}\n" - ) + raise IndexError(f"\nValidate the first line does not begin with a space\n{line}\n") parents = self._current_parents[:-deindent_level] or (self._current_parents[0],) return parents @@ -419,9 +417,7 @@ def find_children_w_parents( ] for cfg_line in self.build_config_relationship(): parents = cfg_line.parents[0] if cfg_line.parents else None - if parents in potential_parents and self._match_type_check( - parents, parent_pattern, match_type - ): + if parents in potential_parents and self._match_type_check(parents, parent_pattern, match_type): config.append(cfg_line.config_line) return config @@ -438,11 +434,7 @@ def config_lines_only(self) -> str: Returns: The non-space lines from ``config``. """ - config_lines = [ - line.rstrip() - for line in self.config.splitlines() - if line and not line.isspace() - ] + config_lines = [line.rstrip() for line in self.config.splitlines() if line and not line.isspace()] return "\n".join(config_lines) def build_config_relationship(self) -> t.List[ConfigLine]: @@ -520,9 +512,7 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: for line in self.generator_config: multiline_config.append(line) if line.lstrip() == delimiter: - multiline_entry = ConfigLine( - "\n".join(multiline_config), self._current_parents - ) + multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) self.config_lines.append(multiline_entry) self._current_parents = self._current_parents[:-1] return multiline_entry @@ -532,9 +522,7 @@ def _build_multiline_config(self, delimiter: str) -> t.Optional[ConfigLine]: class CiscoConfigParser(BaseSpaceConfigParser): """Cisco Implementation of ConfigParser Class.""" - regex_banner = re.compile( - r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)" - ) + regex_banner = re.compile(r"^(banner\s+\S+|\s*vacant-message)\s+(?P\^C|.)") def __init__(self, config: str): """Create ConfigParser Object. @@ -596,9 +584,7 @@ def banner_end(self) -> str: def banner_end(self, banner_start_line: str) -> None: banner_parsed = self.regex_banner.match(banner_start_line) if not banner_parsed: - raise ValueError( - "There was an error parsing your banner, the end of the banner could not be found" - ) + raise ValueError("There was an error parsing your banner, the end of the banner could not be found") self._banner_end = banner_parsed.groupdict()["banner_delimiter"] @@ -867,9 +853,7 @@ def build_config_relationship(self) -> t.List[ConfigLine]: return self.config_lines - def _build_multiline_single_configuration_line( - self, delimiter: str, prev_line: str - ) -> t.Optional[ConfigLine]: + def _build_multiline_single_configuration_line(self, delimiter: str, prev_line: str) -> t.Optional[ConfigLine]: r"""Concatenate Multiline strings between delimiter when newlines causes string to traverse multiple lines. Args: @@ -907,9 +891,7 @@ def _build_multiline_single_configuration_line( for line in self.generator_config: multiline_config.append(line) if line.endswith(delimiter): - multiline_entry = ConfigLine( - "\n".join(multiline_config), self._current_parents - ) + multiline_entry = ConfigLine("\n".join(multiline_config), self._current_parents) self.config_lines[-1] = multiline_entry self._current_parents = self._current_parents[:-1] return multiline_entry @@ -1072,9 +1054,7 @@ def _parse_out_offending(self, config: str) -> str: # This will grab everything between quotes after the 'set buffer' sub-command. # Its explicitly looking for "\n to end the captured data. This is to support html # data that is supported in Fortinet config with double quotes within the html. - pattern = ( - r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" - ) + pattern = r"(config system replacemsg.*(\".*\")\n)(\s{4}set\sbuffer\s\"[\S\s]*?\"\n)" return re.sub(pattern, r"\1 [\2]\n", config) @property @@ -1090,10 +1070,7 @@ def config_lines_only(self) -> str: config_lines = ( line.rstrip() for line in self.config.splitlines() - if line - and not self.is_comment(line) - and not line.isspace() - and not self.is_end_next(line) + if line and not self.is_comment(line) and not line.isspace() and not self.is_end_next(line) ) self._config = "\n".join(config_lines) return self._config @@ -1493,12 +1470,7 @@ class PaloAltoNetworksConfigParser(BaseSpaceConfigParser): def is_banner_end(self, line: str) -> bool: """Determine if end of banner.""" - if ( - line.endswith('"') - or line.startswith('";') - or line.startswith("set") - or line.endswith(self.banner_end) - ): + if line.endswith('"') or line.startswith('";') or line.startswith("set") or line.endswith(self.banner_end): return True return False @@ -1566,9 +1538,7 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to if line.endswith("{"): _needs_conversion = True if _needs_conversion: - converted_config = paloalto_panos_brace_to_set( - cfg=self.config, cfg_type="string" - ) + converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string") list_config = converted_config.splitlines() self.generator_config = (line for line in list_config) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 193558b6..26a50963 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -7,9 +7,7 @@ from netutils.config import compliance -MOCK_DIR = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "mock", "config", "parser" -) +MOCK_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mock", "config", "parser") TXT_FILE = "_sent.txt" base_parameters = [] @@ -20,9 +18,7 @@ base_parameters.append([_file, network_os]) for _file in glob.glob(f"{MOCK_DIR}/find_all_children/{network_os}/*{TXT_FILE}"): find_all_children_parameters.append([_file, network_os]) - for _file in glob.glob( - f"{MOCK_DIR}/find_children_w_parents/{network_os}/*{TXT_FILE}" - ): + for _file in glob.glob(f"{MOCK_DIR}/find_children_w_parents/{network_os}/*{TXT_FILE}"): find_children_w_parents_parameters.append([_file, network_os]) @@ -41,32 +37,23 @@ def test_find_all_children(_file, network_os, get_text_data, get_json_data): # truncate_file = os.path.join(MOCK_DIR, "find_all_children", _file[: -len(TXT_FILE)]) device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_all_children", _file)) - received_data = get_text_data( - os.path.join(MOCK_DIR, "find_all_children", truncate_file + "_received.txt") - ) + received_data = get_text_data(os.path.join(MOCK_DIR, "find_all_children", truncate_file + "_received.txt")) kwargs = get_json_data(truncate_file + "_args.json") os_parser = compliance.parser_map[network_os] assert "\n".join(os_parser(device_cfg).find_all_children(**kwargs)) == received_data @pytest.mark.parametrize("_file, network_os", find_children_w_parents_parameters) -def test_find_children_w_parents(_file, network_os, get_text_data, get_json_data): # pylint: disable=redefined-outer-name - truncate_file = os.path.join( - MOCK_DIR, "find_children_w_parents", _file[: -len(TXT_FILE)] - ) +def test_find_children_w_parents( + _file, network_os, get_text_data, get_json_data +): # pylint: disable=redefined-outer-name + truncate_file = os.path.join(MOCK_DIR, "find_children_w_parents", _file[: -len(TXT_FILE)]) device_cfg = get_text_data(os.path.join(MOCK_DIR, "find_children_w_parents", _file)) - received_data = get_text_data( - os.path.join( - MOCK_DIR, "find_children_w_parents", truncate_file + "_received.txt" - ) - ) + received_data = get_text_data(os.path.join(MOCK_DIR, "find_children_w_parents", truncate_file + "_received.txt")) kwargs = get_json_data(truncate_file + "_args.json") os_parser = compliance.parser_map[network_os] - assert ( - "\n".join(os_parser(device_cfg).find_children_w_parents(**kwargs)) - == received_data - ) + assert "\n".join(os_parser(device_cfg).find_children_w_parents(**kwargs)) == received_data def test_incorrect_banner_ios(): @@ -91,25 +78,5 @@ def test_duplicate_line(): "snmp-server community <> RO SNMP_ACL_RO\n" "snmp-server community <> RW SNMP_ACL_RW\n" ) - with pytest.raises( - IndexError, match=r".*This error is likely from a duplicate line detected.*" - ): + with pytest.raises(IndexError, match=r".*This error is likely from a duplicate line detected.*"): compliance.parser_map["cisco_ios"](logging).config_lines # pylint: disable=expression-not-assigned - - -# def test_nested_banner(): -# banner: str = ( -# "group-policy Grs-POLICY attributes\n" " banner value This is an\n" " banner value example nested banner.\n" -# ) - -# generated_config_lines: list[ConfigLine] = compliance.parser_map["cisco_asa"]( -# banner, -# ).config_lines - -# parent: str = "group-policy Grs-POLICY attributes" -# mock_config_lines: list[ConfigLine] = [ -# ConfigLine(config_line=parent, parents=()), -# ConfigLine(config_line=" banner value This is an", parents=(parent,)), -# ConfigLine(config_line=" banner value example nested banner.", parents=(parent,)), -# ] -# assert generated_config_lines == mock_config_lines From 03f15f255a3d1f58abce33577458327842e5a5b8 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Thu, 19 Dec 2024 15:31:57 -0500 Subject: [PATCH 6/7] Removing parent string from receoved.py asa banner file --- .../base/cisco_asa/asa_nested_banner_received.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py index 05763626..269ec2ad 100644 --- a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py +++ b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py @@ -1,8 +1,13 @@ from netutils.config.parser import ConfigLine -parent: str = "group-policy Grs-POLICY attributes" data: list[ConfigLine] = [ - ConfigLine(config_line=parent, parents=()), - ConfigLine(config_line=" banner value This is an", parents=(parent,)), - ConfigLine(config_line=" banner value example nested banner", parents=(parent,)), + ConfigLine(config_line="group-policy Grs-POLICY attributes", parents=()), + ConfigLine( + config_line=" banner value This is an", + parents=("group-policy Grs-POLICY attributes",), + ), + ConfigLine( + config_line=" banner value example nested banner", + parents=("group-policy Grs-POLICY attributes",), + ), ] From 5da3310ad5c802836289e1521ca1c19f8cbec688 Mon Sep 17 00:00:00 2001 From: Carlos Huaccho Date: Thu, 19 Dec 2024 15:37:51 -0500 Subject: [PATCH 7/7] Removing typing from asa banner received.py file --- .../config/parser/base/cisco_asa/asa_nested_banner_received.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py index 269ec2ad..910e519e 100644 --- a/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py +++ b/tests/unit/mock/config/parser/base/cisco_asa/asa_nested_banner_received.py @@ -1,6 +1,6 @@ from netutils.config.parser import ConfigLine -data: list[ConfigLine] = [ +data = [ ConfigLine(config_line="group-policy Grs-POLICY attributes", parents=()), ConfigLine( config_line=" banner value This is an",