Skip to content

Commit f07cde0

Browse files
authored
Merge pull request #794 from networktocode/u/joewesch-fix-palo-banner-parsing
[NTC-4957] Fixed parsing Palo banners that start on the same line
2 parents b5a3fe7 + 3b458e1 commit f07cde0

37 files changed

+588
-61
lines changed

changes/+invoke.housekeeping

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `--pattern` and `--label` options to the `invoke pytest` task.

changes/780.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed parsing of login banner in Palo Alto Networks config.

netutils/config/conversion.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def paloalto_panos_clean_newlines(cfg: str) -> str:
125125
return newlines_cleaned_cfg
126126

127127

128+
# pylint: disable=too-many-branches
128129
def paloalto_panos_brace_to_set(cfg: str, cfg_type: str = "file") -> str:
129130
r"""Convert Palo Alto Brace format configuration to set format.
130131
@@ -182,21 +183,44 @@ def paloalto_panos_brace_to_set(cfg: str, cfg_type: str = "file") -> str:
182183
cfg_raw = paloalto_panos_clean_newlines(cfg=cfg)
183184
cfg_list = cfg_raw.splitlines()
184185

185-
for i, line in enumerate(cfg_list):
186+
def cfg_generator(cfg_list: t.List[str]) -> t.Generator[str, None, None]:
187+
"""We use a generator to avoid parsing the banner lines twice."""
188+
yield from cfg_list
189+
190+
cfg_gen = cfg_generator(cfg_list)
191+
192+
for line in cfg_gen:
186193
line = line.strip()
187194
if line.endswith(";") and not line.startswith('";'):
188-
line = line.split(";", 1)[0]
195+
line = line[:-1]
189196
line = "".join(str(s) for s in stack) + line
190197
line = line.split("config ", 1)[1]
191198
line = "set " + line
192199
cfg_value.append(line.strip())
193-
elif line.endswith('login-banner "') or line.endswith('content "'):
200+
elif "login-banner" in line or line.endswith('content "'):
194201
_first_banner_line = "".join(str(s) for s in stack) + line
195202
cfg_value.append("set " + _first_banner_line.split("config ", 1)[1])
196-
197-
for banner_line in cfg_list[i + 1:]: # fmt: skip
198-
if '"' in banner_line:
199-
banner_line = banner_line.split(";", 1)[0]
203+
# Palo Alto uses either double or single quotes for the banner delimiter,
204+
# but only if there are certain characters or spaces in the banner.
205+
if 'login-banner "' in line:
206+
delimiter = '"'
207+
elif "login-banner '" in line:
208+
delimiter = "'"
209+
else:
210+
delimiter = ""
211+
212+
# Deal with single line banners first
213+
if line.endswith(f"{delimiter};"):
214+
line = line[:-1]
215+
cfg_value.append(line.strip())
216+
continue
217+
218+
# Multi-line banners
219+
for banner_line in cfg_gen: # fmt: skip
220+
# This is a little brittle and will break if any line in the middle of the banner
221+
# ends with the expected delimiter and semicolon.
222+
if banner_line.endswith(f"{delimiter};"):
223+
banner_line = banner_line[:-1]
200224
cfg_value.append(banner_line.strip())
201225
break
202226
cfg_value.append(banner_line.strip())

netutils/config/parser.py

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,17 +1492,11 @@ class PaloAltoNetworksConfigParser(BaseSpaceConfigParser):
14921492

14931493
comment_chars: t.List[str] = []
14941494
banner_start: t.List[str] = [
1495-
'set system login-banner "',
1496-
'login-banner "',
1497-
'set deviceconfig system login-banner "',
1495+
"set system login-banner",
1496+
"set deviceconfig system login-banner",
14981497
]
1499-
banner_end = '"'
1500-
1501-
def is_banner_end(self, line: str) -> bool:
1502-
"""Determine if end of banner."""
1503-
if line.endswith('"') or line.startswith('";') or line.startswith("set") or line.endswith(self.banner_end):
1504-
return True
1505-
return False
1498+
# Not used, but must be defined
1499+
banner_end = ""
15061500

15071501
def _build_banner(self, config_line: str) -> t.Optional[str]:
15081502
"""Handle banner config lines.
@@ -1518,24 +1512,24 @@ def _build_banner(self, config_line: str) -> t.Optional[str]:
15181512
"""
15191513
self._update_config_lines(config_line)
15201514
self._current_parents += (config_line,)
1521-
banner_config = []
1515+
banner_config: t.List[str] = []
15221516
for line in self.generator_config:
1523-
if not self.is_banner_end(line):
1524-
banner_config.append(line)
1525-
else:
1526-
banner_config.append(line.strip())
1527-
line = "\n".join(banner_config)
1528-
if line.endswith('"'):
1529-
banner, end, _ = line.rpartition('"')
1530-
line = banner + end
1531-
self._update_config_lines(line.strip())
1517+
# Note, this is a little fragile and will cause false positives if any line in
1518+
# the middle of a multi-line banner starts with "set ".
1519+
if line.startswith("set "):
1520+
# New command, save the banner and return the next line
1521+
if banner_config:
1522+
banner_string = "\n".join(banner_config)
1523+
self._update_config_lines(banner_string)
15321524
self._current_parents = self._current_parents[:-1]
1533-
try:
1534-
return next(self.generator_config)
1535-
except StopIteration:
1536-
return None
1525+
return line
1526+
banner_config.append(line)
15371527

1538-
raise ValueError("Unable to parse banner end.")
1528+
# Edge case, the last line of the config is the banner
1529+
banner_string = "\n".join(banner_config)
1530+
self._update_config_lines(banner_string)
1531+
self._current_parents = self._current_parents[:-1]
1532+
return None
15391533

15401534
def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=too-many-branches
15411535
r"""Parse text of config lines and find their parents.
@@ -1558,42 +1552,27 @@ def build_config_relationship(self) -> t.List[ConfigLine]: # pylint: disable=to
15581552
... ]
15591553
True
15601554
"""
1561-
# assume configuration does not need conversion
1562-
_needs_conversion = False
1555+
if self.config_lines_only is None:
1556+
raise ValueError("Config is empty.")
15631557

1564-
# if config is in palo brace format, convert to set
1565-
if self.config_lines_only is not None:
1566-
for line in self.config_lines_only.splitlines():
1567-
if line.endswith("{"):
1568-
_needs_conversion = True
1569-
if _needs_conversion:
1558+
if "@dirtyId" in self.config_lines_only:
1559+
# We have to specifically check for JSON format because it can be confused with the brace format
1560+
raise ValueError("Found 'json' configuration format. Please provide in 'set' or 'default' (brace) format.")
1561+
config_lines = self.config_lines_only.splitlines()
1562+
if any(line.endswith("{") for line in config_lines):
15701563
converted_config = paloalto_panos_brace_to_set(cfg=self.config, cfg_type="string")
15711564
list_config = converted_config.splitlines()
15721565
self.generator_config = (line for line in list_config)
1566+
elif not any(line.startswith("set ") for line in config_lines):
1567+
raise ValueError("Unexpected configuration format. Please provide in 'set' or 'default' (brace) format.")
15731568

15741569
# build config relationships
15751570
for line in self.generator_config:
1576-
if not line[0].isspace():
1577-
self._current_parents = ()
1578-
if self.is_banner_start(line):
1579-
line = self._build_banner(line) # type: ignore
1580-
else:
1581-
previous_config = self.config_lines[-1]
1582-
self._current_parents = (previous_config.config_line,)
1583-
self.indent_level = self.get_leading_space_count(line)
1584-
if not self.is_banner_start(line):
1585-
line = self._build_nested_config(line) # type: ignore
1586-
else:
1587-
line = self._build_banner(line) # type: ignore
1588-
if line is not None and line[0].isspace():
1589-
line = self._build_nested_config(line) # type: ignore
1590-
else:
1591-
self._current_parents = ()
1571+
if self.is_banner_start(line):
1572+
line = self._build_banner(line) # type: ignore
15921573

15931574
if line is None:
15941575
break
1595-
elif self.is_banner_start(line):
1596-
line = self._build_banner(line) # type: ignore
15971576

15981577
self._update_config_lines(line)
15991578
return self.config_lines

tasks.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,23 @@ def coverage(context):
169169
run_command(context, "coverage html")
170170

171171

172-
@task
173-
def pytest(context):
172+
@task(
173+
help={
174+
"pattern": "Only run tests which match the given substring. Can be used multiple times.",
175+
"label": "Module path to run (e.g., tests/unit/test_foo.py). Can be used multiple times.",
176+
},
177+
iterable=["pattern", "label"],
178+
)
179+
def pytest(context, pattern=None, label=None):
174180
"""Run pytest test cases."""
175-
exec_cmd = "pytest -vv --doctest-modules netutils/ && coverage run --source=netutils -m pytest && coverage report"
181+
doc_test_cmd = "pytest -vv --doctest-modules netutils/"
182+
pytest_cmd = "coverage run --source=netutils -m pytest"
183+
if pattern:
184+
pytest_cmd += "".join([f" -k {_pattern}" for _pattern in pattern])
185+
if label:
186+
pytest_cmd += "".join([f" {_label}" for _label in label])
187+
coverage_cmd = "coverage report"
188+
exec_cmd = " && ".join([doc_test_cmd, pytest_cmd, coverage_cmd])
176189
run_command(context, exec_cmd)
177190

178191

tests/unit/mock/config/compliance/compliance/paloalto_panos/paloalto_backup.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ set deviceconfig system service disable-http yes
2121
set deviceconfig system service disable-snmp no
2222
set deviceconfig system snmp-setting snmp-system
2323
set deviceconfig system hostname firewall1
24+
set deviceconfig system login-banner "
25+
************************************************************************
26+
* firewall1.example.com * [PROD VM500 firewalls]
27+
************************************************************************
28+
* WARNING *
29+
* Unauthorized access to this device or devices attached to *
30+
* or accessible from this network is strictly prohibited. *
31+
* Possession of passwords or devices enabling access to this *
32+
* device or devices does not constitute authorization. Unauthorized *
33+
* access will be prosecuted to the fullest extent of the law. *
34+
* *
35+
************************************************************************
36+
37+
"
2438
set deviceconfig system default-gateway 10.1.1.1
2539
set deviceconfig system dns-setting servers primary 10.1.1.3
2640
set deviceconfig system dns-setting servers secondary 10.1.1.4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
features = [
22
{"name": "management", "ordered": False, "section": ["set mgt-config "]},
3+
{"name": "banner", "ordered": False, "section": ["set deviceconfig system login-banner "]},
34
]

tests/unit/mock/config/compliance/compliance/paloalto_panos/paloalto_intended.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ set deviceconfig system service disable-http yes
2121
set deviceconfig system service disable-snmp no
2222
set deviceconfig system snmp-setting snmp-system
2323
set deviceconfig system hostname firewall1
24+
set deviceconfig system login-banner "####################################################
25+
WARNING TO UNAUTHORIZED USERS:
26+
This system is for use by authorized users only.
27+
Any individual using this system, by such use,
28+
acknowledges and consents to the right of the
29+
company to monitor, access, use, and disclose any
30+
information generated, received, or stored on the
31+
systems, and waives any right of privacy or
32+
expectation of privacy on the part of that
33+
individual in connection with his or her use of
34+
this system. Unauthorized and/or improper use of
35+
this system, as delineated by corporate policies,
36+
is not tolerated and the company may take formal
37+
action against such individuals.
38+
####################################################
39+
"
2440
set deviceconfig system default-gateway 10.1.1.1
2541
set deviceconfig system dns-setting servers primary 10.1.1.3
2642
set deviceconfig system dns-setting servers secondary 10.1.1.4

tests/unit/mock/config/compliance/compliance/paloalto_panos/paloalto_received.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@
88
"missing": "",
99
"ordered_compliant": true,
1010
"unordered_compliant": true
11+
},
12+
"banner": {
13+
"compliant": false,
14+
"missing": "set deviceconfig system login-banner \"####################################################\nWARNING TO UNAUTHORIZED USERS:\nThis system is for use by authorized users only.\nAny individual using this system, by such use,\nacknowledges and consents to the right of the\ncompany to monitor, access, use, and disclose any\ninformation generated, received, or stored on the\nsystems, and waives any right of privacy or\nexpectation of privacy on the part of that\nindividual in connection with his or her use of\nthis system. Unauthorized and/or improper use of\nthis system, as delineated by corporate policies,\nis not tolerated and the company may take formal\naction against such individuals.\n####################################################\n\"",
15+
"extra": "set deviceconfig system login-banner \"\n************************************************************************\n* firewall1.example.com * [PROD VM500 firewalls]\n************************************************************************\n* WARNING *\n* Unauthorized access to this device or devices attached to *\n* or accessible from this network is strictly prohibited. *\n* Possession of passwords or devices enabling access to this *\n* device or devices does not constitute authorization. Unauthorized *\n* access will be prosecuted to the fullest extent of the law. *\n* *\n************************************************************************\n\"",
16+
"cannot_parse": true,
17+
"unordered_compliant": false,
18+
"ordered_compliant": false,
19+
"actual": "set deviceconfig system login-banner \"\n************************************************************************\n* firewall1.example.com * [PROD VM500 firewalls]\n************************************************************************\n* WARNING *\n* Unauthorized access to this device or devices attached to *\n* or accessible from this network is strictly prohibited. *\n* Possession of passwords or devices enabling access to this *\n* device or devices does not constitute authorization. Unauthorized *\n* access will be prosecuted to the fullest extent of the law. *\n* *\n************************************************************************\n\"",
20+
"intended": "set deviceconfig system login-banner \"####################################################\nWARNING TO UNAUTHORIZED USERS:\nThis system is for use by authorized users only.\nAny individual using this system, by such use,\nacknowledges and consents to the right of the\ncompany to monitor, access, use, and disclose any\ninformation generated, received, or stored on the\nsystems, and waives any right of privacy or\nexpectation of privacy on the part of that\nindividual in connection with his or her use of\nthis system. Unauthorized and/or improper use of\nthis system, as delineated by corporate policies,\nis not tolerated and the company may take formal\naction against such individuals.\n####################################################\n\""
1121
}
1222
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
set deviceconfig system hostname pa-ntc
2+
set deviceconfig system login-banner '"BANNER"'
3+
set deviceconfig system domain ntc

0 commit comments

Comments
 (0)