Skip to content

Commit

Permalink
feat: Check for April PAN-OS Certificate Advisory (#143)
Browse files Browse the repository at this point in the history
Co-authored-by: Alp Kose <[email protected]>
  • Loading branch information
adambaumeister and alperenkose authored Jan 28, 2024
1 parent fee7aef commit af94d0e
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 15 deletions.
53 changes: 53 additions & 0 deletions docs/panos-upgrade-assurance/api/check_firewall.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,34 @@ __Returns__

`dict`: Results of all configured checks.

### `CheckFirewall.check_version_against_version_match_dict`

```python
@staticmethod
def check_version_against_version_match_dict(version: Version,
match_dict: dict) -> bool
```

Compare the given software version against the match dict.

__Parameters__


- __version__ (`Version`): The software version to compare (e.g. "10.1.11").
- __match_dict__ (`dict`): A dictionary of tuples mapping major/minor versions to match criteria:

```python showLineNumbers title="Example"
{
"81": [("==", "8.1.21.2"), (">=", "8.1.25.1")],
"90": [(">=", "9.0.16.5")],
}
```

__Returns__


`bool`: `True` If the given software version matches the provided match criteria

### `CheckFirewall.check_device_root_certificate_issue`

```python
Expand Down Expand Up @@ -721,3 +749,28 @@ __Returns__
* [`CheckStatus.SUCCESS`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) if the device is not affected,
* [`CheckStatus.FAIL`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) otherwise.

### `CheckFirewall.check_cdss_and_panorama_certificate_issue`

```python
def check_cdss_and_panorama_certificate_issue() -> CheckResult
```

Checks whether the device is affected by the [PAN-OS Certificate Expirations Jan 2024 advisory][live-572158].

[live-572158]: https://live.paloaltonetworks.com/t5/customer-advisories/additional-pan-os-certificate-expirations-and-new-comprehensive/ta-p/572158

Check will fail in either of following scenarios:

* Device is running an affected software version
* Device is running an affected content version
* Device is running the fixed content version or higher but has not been rebooted - note this is best effort,
and is based on when the content version was released and the device was rebooted

__Returns__


`CheckResult`: Object of [`CheckResult`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkresult) class taking value of:

* [`CheckStatus.SUCCESS`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) if the device is not affected,
* [`CheckStatus.FAIL`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) otherwise.

19 changes: 19 additions & 0 deletions docs/panos-upgrade-assurance/api/firewall_proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -1257,3 +1257,22 @@ __Returns__
}
```

### `FirewallProxy.get_system_time_rebooted`

```python
def get_system_time_rebooted() -> datetime
```

Returns the date and time the system last rebooted using the system uptime.

The actual API command is `show system info`.

__Returns__


`datetime`: Time system was last rebooted based on current time - system uptime string

```python showLineNumbers title="Sample output"
datetime(2024, 01, 01, 00, 00, 00)
```

4 changes: 3 additions & 1 deletion examples/readiness_checks/run_health_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
vsys = args.vsys

if serial:
print(address)
panorama = Panorama(
hostname=address, api_password=password, api_username=username
)
Expand All @@ -79,7 +80,8 @@
check_node = CheckFirewall(firewall)

checks = [
"device_root_certificate_issue"
"device_root_certificate_issue",
"cdss_and_panorama_certificate_issue"
]

check_health = check_node.run_health_checks(
Expand Down
148 changes: 135 additions & 13 deletions panos_upgrade_assurance/check_firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import panos.errors
from packaging.version import parse as parse_version
from packaging.version import Version

from panos_upgrade_assurance.utils import (
CheckResult,
Expand Down Expand Up @@ -104,7 +105,10 @@ def __init__(self, node: FirewallProxy, skip_force_locale: Optional[bool] = Fals
CheckType.JOBS: self.check_non_finished_jobs,
}

self._health_check_method_mapping = {HealthType.DEVICE_ROOT_CERTIFICATE_ISSUE: self.check_device_root_certificate_issue}
self._health_check_method_mapping = {
HealthType.DEVICE_ROOT_CERTIFICATE_ISSUE: self.check_device_root_certificate_issue,
HealthType.DEVICE_CDSS_AND_PANORAMA_CERTIFICATE_ISSUE: self.check_cdss_and_panorama_certificate_issue,
}

if not skip_force_locale:
locale.setlocale(
Expand Down Expand Up @@ -1253,6 +1257,39 @@ def run_health_checks(

return result

@staticmethod
def check_version_against_version_match_dict(version: Version, match_dict: dict) -> bool:
"""Compare the given software version against the match dict.
# Parameters
version (Version): The software version to compare (e.g. "10.1.11").
match_dict (dict): A dictionary of tuples mapping major/minor versions to match criteria:
```python showLineNumbers title="Example"
{
"81": [("==", "8.1.21.2"), (">=", "8.1.25.1")],
"90": [(">=", "9.0.16.5")],
}
```
# Returns
bool: `True` If the given software version matches the provided match criteria
"""
match_versions = match_dict.get(f"{version.major}{version.minor}")
if match_versions:
for operator, match_version in match_versions:
match_version = parse_version(match_version)
if operator == "==":
if version == match_version:
return True
elif operator == ">=":
if version >= match_version:
return True
return False

def check_device_root_certificate_issue(self, fail_when_affected_version_only: bool = True) -> CheckResult:
"""Checks whether the target device is affected by the [Root Certificate Expiration][live-564672] issue.
Expand Down Expand Up @@ -1280,6 +1317,7 @@ def check_device_root_certificate_issue(self, fail_when_affected_version_only: b
* [`CheckStatus.SUCCESS`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) if the device is not affected,
* [`CheckStatus.FAIL`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) otherwise.
"""
result = CheckResult()

Expand Down Expand Up @@ -1325,19 +1363,9 @@ def check_device_root_certificate_issue(self, fail_when_affected_version_only: b
}
fixed_content_version = 8776.8390

fixed_versions = fixed_version_map.get(f"{software_version.major}{software_version.minor}")
if fixed_versions:
for operator, fixed_version in fixed_versions:
fixed_version = parse_version(fixed_version)
if operator == "==":
if software_version == fixed_version:
result.status = CheckStatus.SUCCESS
elif operator == ">=":
if software_version >= fixed_version:
result.status = CheckStatus.SUCCESS

# If the device is already running fixed software, we can return immediately
if result.status == CheckStatus.SUCCESS:
if self.check_version_against_version_match_dict(software_version, fixed_version_map):
result.status = CheckStatus.SUCCESS
return result

# Return if this check is just looking at the software and not implementing any other checks
Expand Down Expand Up @@ -1381,3 +1409,97 @@ def check_device_root_certificate_issue(self, fail_when_affected_version_only: b
"expire December 31st, 2023."
)
return result

def check_cdss_and_panorama_certificate_issue(self) -> CheckResult:
"""Checks whether the device is affected by the [PAN-OS Certificate Expirations Jan 2024 advisory][live-572158].
[live-572158]: https://live.paloaltonetworks.com/t5/customer-advisories/additional-pan-os-certificate-expirations-and-new-comprehensive/ta-p/572158
Check will fail in either of following scenarios:
* Device is running an affected software version
* Device is running an affected content version
* Device is running the fixed content version or higher but has not been rebooted - note this is best effort,
and is based on when the content version was released and the device was rebooted
# Returns
CheckResult: Object of [`CheckResult`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkresult) class taking \
value of:
* [`CheckStatus.SUCCESS`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) if the device is not affected,
* [`CheckStatus.FAIL`](/panos/docs/panos-upgrade-assurance/api/utils#class-checkstatus) otherwise.
"""
fixed_version_map = {
"81": [("==", "8.1.21.3"), ("==", "8.1.25.3"), (">=", "8.1.26")],
"90": [("==", "9.0.16.7"), ("==", "9.0.17.5")],
"91": [
("==", "9.1.11.5"),
("==", "9.1.12.7"),
("==", "9.1.13.5"),
("==", "9.1.14.8"),
("==", "9.1.16.5"),
(">=", "9.1.17"),
],
"100": [("==", "10.0.8.11"), ("==", "10.0.11.4"), ("==", "10.0.12.5")],
"101": [
("==", "10.1.3.3"),
("==", "10.1.4.6"),
("==", "10.1.5.4"),
("==", "10.1.6.8"),
("==", "10.1.7.1"),
("==", "10.1.8.7"),
("==", "10.1.9.8"),
("==", "10.1.10.5"),
("==", "10.1.11.4"),
(">=", "10.1.12"),
],
"102": [
("==", "10.2.0.2"),
("==", "10.2.1.1"),
("==", "10.2.2.4"),
("==", "10.2.3.11"),
("==", "10.2.4.10"),
("==", "10.2.5.4"),
("==", "10.2.6.1"),
("==", "10.2.7.3"),
(">=", "10.2.8"),
],
"110": [("==", "11.0.0.2"), ("==", "11.0.1.3"), ("==", "11.0.2.3"), (">=", "11.0.3.3"), (">=", "11.0.4")],
"111": [("==", "11.1.0.2"), (">=", "11.1.1")],
}

# Release date and fixed version are both static
fixed_content_version = 8795.8489
fixed_content_version_release_date = datetime(2024, 1, 8, 19, 26, 43)

result = CheckResult()

software_version = self._node.get_device_software_version()

if self.check_version_against_version_match_dict(software_version, fixed_version_map):
# Fixed software means we can return immediately, no need to further check
result.status = CheckStatus.SUCCESS
return result

content_version = float(self._node.get_content_db_version().replace("-", "."))

if content_version >= fixed_content_version:
# Check the device has been rebooted since the release of the fixed content version
# This is not a perfect test - if the customer reboots without installing the content update, then
# later installs it, it will pass even though one further restart is required.
reboot_time = self._node.get_system_time_rebooted()
if reboot_time < fixed_content_version_release_date:
result.reason = "Device is running fixed Content but still requires a restart for the fix to take " "effect."
return result
else:
result.status = CheckStatus.SUCCESS
return result

result.reason = (
"Device is running a software version, and a content version, that is affected by the 2024 certificate"
" expiration, the first of which will occur on the 7th of April, 2024."
)

return result
31 changes: 30 additions & 1 deletion panos_upgrade_assurance/firewall_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pan.xapi import PanXapiError
from panos_upgrade_assurance import exceptions
from math import floor
from datetime import datetime
from datetime import datetime, timedelta
from packaging import version


Expand Down Expand Up @@ -1462,3 +1462,32 @@ def get_fib(self) -> dict:
results[key] = result_entry

return results

def get_system_time_rebooted(self) -> datetime:
"""Returns the date and time the system last rebooted using the system uptime.
The actual API command is `show system info`.
# Returns
datetime: Time system was last rebooted based on current time - system uptime string
```python showLineNumbers title="Sample output"
datetime(2024, 01, 01, 00, 00, 00)
```
"""
response = self.op_parser(cmd="show system info", return_xml=True)
uptime_string = response.findtext("./system/uptime")
current_time = datetime.now()

time_re_match = re.search(r"(\d+) days, (\d+):(\d+):(\d+)", uptime_string)

rebooted_time = current_time - timedelta(
days=int(time_re_match.group(1)),
hours=int(time_re_match.group(2)),
minutes=int(time_re_match.group(3)),
seconds=int(time_re_match.group(4)),
)

return rebooted_time
1 change: 1 addition & 0 deletions panos_upgrade_assurance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class HealthType:
"""

DEVICE_ROOT_CERTIFICATE_ISSUE = "device_root_certificate_issue"
DEVICE_CDSS_AND_PANORAMA_CERTIFICATE_ISSUE = "cdss_and_panorama_certificate_issue"


class CheckStatus(Enum):
Expand Down
42 changes: 42 additions & 0 deletions tests/test_check_firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -1307,3 +1307,45 @@ def test_run_health_checks(self, check_firewall_mock):

check_firewall_mock._health_check_method_mapping["check1"].assert_called_once_with()
check_firewall_mock._health_check_method_mapping["check2"].assert_called_once_with(param1=123)

@pytest.mark.parametrize(
"running_software, expected_status",
[
("10.1.2", CheckStatus.FAIL), # Device running broken version
("10.1.13", CheckStatus.SUCCESS), # Device running fixed version
],
)
def test_check_cdss_and_panorama_certificate_issue(self, running_software, expected_status, check_firewall_mock):
"""This test validates the behavior when the test is only checking the software version is affected by
the issue."""

from packaging import version

check_firewall_mock._node.get_device_software_version = MagicMock(return_value=version.parse(running_software))
assert check_firewall_mock.check_cdss_and_panorama_certificate_issue().status == expected_status

@pytest.mark.parametrize(
"running_content_version, last_reboot, expected_status",
[
("8000-8391", datetime(2022, 1, 1, 0, 0, 0), CheckStatus.FAIL), # Device running older content version and no reboot
("8795-8489", datetime(2022, 1, 1, 0, 0, 0), CheckStatus.FAIL), # Device running fixed version without reboot
("8795-8489", datetime(2024, 1, 10, 0, 0, 0), CheckStatus.SUCCESS), # Device running fixed version and rebooted
],
)
def test_check_cdss_and_panorama_certificate_issue_by_content_version(
self, running_content_version, last_reboot, expected_status, check_firewall_mock
):
"""Tests that we check the content version and use a best effort approach for seeing if the device has been
rebooted in the time since it was released/installed"""
from packaging import version

check_firewall_mock._node.get_device_software_version = MagicMock(
return_value=version.parse("10.1.0") # Affected Version
)

check_firewall_mock._node.get_content_db_version = MagicMock(return_value=running_content_version)

# Device hasn't been rebooted
check_firewall_mock._node.get_system_time_rebooted = MagicMock(return_value=last_reboot)

assert check_firewall_mock.check_cdss_and_panorama_certificate_issue().status == expected_status
Loading

0 comments on commit af94d0e

Please sign in to comment.