diff --git a/device_certificate_report/components/data_collection.py b/device_certificate_report/components/data_collection.py index b6aa486..cf683bc 100644 --- a/device_certificate_report/components/data_collection.py +++ b/device_certificate_report/components/data_collection.py @@ -70,22 +70,26 @@ def process_csv_file(csv_file: str) -> List[DeviceInfo]: device = DeviceInfo( device_name=device_names[i] if i < len(device_names) else None, model=model or None, - serial_number=serial_numbers[i] - if i < len(serial_numbers) - else None, + serial_number=( + serial_numbers[i] if i < len(serial_numbers) else None + ), ipv4_address=ipv4_addresses[i] if i < len(ipv4_addresses) else None, device_state=device_states[i] if i < len(device_states) else None, - device_certificate=device_certificates[i] - if i < len(device_certificates) - else None, - device_certificate_expiry_date=device_certificate_expiry_dates[i] - if i < len(device_certificate_expiry_dates) - else None, + device_certificate=( + device_certificates[i] if i < len(device_certificates) else None + ), + device_certificate_expiry_date=( + device_certificate_expiry_dates[i] + if i < len(device_certificate_expiry_dates) + else None + ), software_version=software_version or None, - globalprotect_client=globalprotect_clients[i] - if i < len(globalprotect_clients) - and globalprotect_clients[i] != "0.0.0" - else None, + globalprotect_client=( + globalprotect_clients[i] + if i < len(globalprotect_clients) + and globalprotect_clients[i] != "0.0.0" + else None + ), ) devices.append(device) @@ -147,9 +151,9 @@ def collect_data_from_panorama(panorama: Panorama) -> List[DeviceInfo]: device_certificate=device_certificate or "", device_certificate_expiry_date=device_certificate_expiry_date or "", software_version=software_version or "", - globalprotect_client=globalprotect_client - if globalprotect_client != "0.0.0" - else "", + globalprotect_client=( + globalprotect_client if globalprotect_client != "0.0.0" else "" + ), ) devices.append(device) except Exception as e: @@ -256,8 +260,8 @@ def collect_data_from_firewall(firewall: Firewall) -> DeviceInfo: device_certificate=device_certificate_status or "", device_certificate_expiry_date=device_certificate_expiry_date or "", software_version=software_version or "", - globalprotect_client=globalprotect_client - if globalprotect_client != "0.0.0" - else "", + globalprotect_client=( + globalprotect_client if globalprotect_client != "0.0.0" else "" + ), ) return device diff --git a/device_certificate_report/main.py b/device_certificate_report/main.py index 4026851..5a30dcb 100644 --- a/device_certificate_report/main.py +++ b/device_certificate_report/main.py @@ -315,7 +315,9 @@ def firewall( devices_with_certificates=devices_with_certificates, output_file=output_file if len(output_file) > 0 else f"{hostname}.pdf", ) - typer.echo(f"Report generated at {output_file if len(output_file) > 0 else f'{hostname}.pdf'}") + typer.echo( + f"Report generated at {output_file if len(output_file) > 0 else f'{hostname}.pdf'}" + ) except Exception as e: logger.error(f"Failed to process Firewall: {e}") sys.exit(1) diff --git a/device_certificate_report/utilities/filters.py b/device_certificate_report/utilities/filters.py index 0483c46..95bf0dc 100644 --- a/device_certificate_report/utilities/filters.py +++ b/device_certificate_report/utilities/filters.py @@ -1,7 +1,10 @@ # device_certificate_report/components/filters.py from typing import List, Tuple -from device_certificate_report.config.hardware_families import AffectedModels, UnaffectedModels +from device_certificate_report.config.hardware_families import ( + AffectedModels, + UnaffectedModels, +) from device_certificate_report.config.panos_versions import MinimumPatchedVersions from device_certificate_report.models.device import DeviceInfo from device_certificate_report.components.version import parse_version diff --git a/pyproject.toml b/pyproject.toml index 14616c7..a0c882f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,9 @@ mkdocs = "^1.6.1" mkdocs-material = "^9.5.35" pytest = "^8.3.3" ipdb = "^0.13.13" +black = "^24.8.0" +flake8 = "^7.1.1" +factory-boy = "^3.3.1" [build-system] requires = ["poetry-core"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ec9e3b8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +# tests/conftest.py + +import pytest +from tests.factories import DeviceInfoFactory + +@pytest.fixture +def sample_device(): + return DeviceInfoFactory() \ No newline at end of file diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..9d49b36 --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,20 @@ +# tests/factories.py + +import factory +from device_certificate_report.models.device import DeviceInfo + +class DeviceInfoFactory(factory.Factory): + class Meta: + model = DeviceInfo + + device_name = factory.Sequence(lambda n: f"device{n}") + model = "PA-220" + serial_number = factory.Sequence(lambda n: f"serial{n}") + ipv4_address = factory.Sequence(lambda n: f"192.168.1.{n}") + device_state = "Connected" + device_certificate = "Valid" + device_certificate_expiry_date = "2024-12-31" + software_version = "10.0.0" + globalprotect_client = "5.2.6" + min_required_version = None + notes = None \ No newline at end of file diff --git a/tests/test_cleaner.py b/tests/test_cleaner.py new file mode 100644 index 0000000..27d14dc --- /dev/null +++ b/tests/test_cleaner.py @@ -0,0 +1,28 @@ +# tests/test_cleaner.py + +from device_certificate_report.utilities.cleaner import clean_html_tags, clean_csv +import csv +import io + +def test_clean_html_tags(): + text = '

"Some;Text";

' + cleaned = clean_html_tags(text) + assert cleaned == '"Some;Text";' + +def test_clean_csv(tmp_path): + input_content = '''"Column1","Column2" +"

Data1

","

Data2

" +"Data;3","Data;4" +''' + input_file = tmp_path / "input.csv" + output_file = tmp_path / "output.csv" + input_file.write_text(input_content) + + clean_csv(str(input_file), str(output_file)) + + with open(output_file, "r", encoding="utf-8") as f: + reader = csv.reader(f) + rows = list(reader) + assert rows[0] == ['Column1', 'Column2'] + assert rows[1] == ['Data1', 'Data2'] + assert rows[2] == ['Data;3', 'Data;4'] \ No newline at end of file diff --git a/tests/test_data_collection.py b/tests/test_data_collection.py new file mode 100644 index 0000000..2bc77e4 --- /dev/null +++ b/tests/test_data_collection.py @@ -0,0 +1,154 @@ +# tests/test_data_collection.py + +import pytest +from unittest.mock import MagicMock +from device_certificate_report.components.data_collection import ( + process_csv_file, + collect_data_from_panorama, + collect_data_from_firewall, +) +from panos.panorama import Panorama +from panos.firewall import Firewall +import xml.etree.ElementTree as ET + +from tests.factories import DeviceInfoFactory + +def test_process_csv_file(tmp_path): + # Create sample devices using factories + device1 = DeviceInfoFactory() + device2 = DeviceInfoFactory( + device_name="device2", + model="PA-3020", + software_version="9.1.0", + globalprotect_client=None, + ) + + # Create CSV content from devices + csv_content = f"""Device Name,IP Address Serial Number,IP Address IPv4,Status Device State,Status Device Certificate,Status Device Certificate Expiry Date,GlobalProtect Client,Model,Software Version +"{device1.device_name}","{device1.serial_number}","{device1.ipv4_address}","{device1.device_state}","{device1.device_certificate}","{device1.device_certificate_expiry_date}","{device1.globalprotect_client or '0.0.0'}","{device1.model}","{device1.software_version}" +"{device2.device_name}","{device2.serial_number}","{device2.ipv4_address}","{device2.device_state}","{device2.device_certificate}","{device2.device_certificate_expiry_date}","{device2.globalprotect_client or '0.0.0'}","{device2.model}","{device2.software_version}" +""" + csv_file = tmp_path / "test.csv" + csv_file.write_text(csv_content) + + devices = process_csv_file(str(csv_file)) + + assert len(devices) == 2 + assert devices[0].device_name == device1.device_name + assert devices[0].model == device1.model + assert devices[0].software_version == device1.software_version + assert devices[1].device_name == device2.device_name + assert devices[1].globalprotect_client is None # Should be None because it's "0.0.0" + +def test_collect_data_from_panorama(monkeypatch): + # Mock Panorama instance + panorama = Panorama("hostname", "username", "password") + + # Create sample devices using factories + device1 = DeviceInfoFactory() + device2 = DeviceInfoFactory( + device_name="device2", + model="PA-3020", + serial_number="serial2", + ipv4_address="192.168.1.2", + device_state="Disconnected", + device_certificate="Invalid", + device_certificate_expiry_date="2023-11-30", + software_version="9.1.0", + globalprotect_client=None, + ) + + # Create XML response using devices + mock_response = ET.fromstring(f""" + + + + + {device1.device_name} + {device1.model} + {device1.serial_number} + {device1.ipv4_address} + {"yes" if device1.device_state == "Connected" else "no"} + {device1.device_certificate} + {device1.device_certificate_expiry_date} + {device1.software_version} + {device1.globalprotect_client or "0.0.0"} + + + {device2.device_name} + {device2.model} + {device2.serial_number} + {device2.ipv4_address} + {"yes" if device2.device_state == "Connected" else "no"} + {device2.device_certificate} + {device2.device_certificate_expiry_date} + {device2.software_version} + {device2.globalprotect_client or "0.0.0"} + + + + + """) + + def mock_op(cmd, cmd_xml=False): + return mock_response + + monkeypatch.setattr(panorama, "op", mock_op) + + devices = collect_data_from_panorama(panorama) + + assert len(devices) == 2 + assert devices[0].device_name == device1.device_name + assert devices[0].device_state == device1.device_state + assert devices[1].device_name == device2.device_name + assert devices[1].device_state == device2.device_state + +def test_collect_data_from_firewall(monkeypatch): + # Mock Firewall instance + firewall = Firewall("hostname", "username", "password") + + # Create a sample device using factory + device = DeviceInfoFactory() + + # Mock responses from firewall.op() + mock_system_info_response = ET.fromstring(f""" + + + + {device.device_name} + {device.model} + {device.serial_number} + {device.ipv4_address} + {device.software_version} + {device.globalprotect_client or "0.0.0"} + {device.device_certificate} + + + + """) + + mock_device_cert_response = ET.fromstring(f""" + + + + {device.device_certificate} + {device.device_certificate_expiry_date} + + + + """) + + def mock_op(cmd, cmd_xml=False): + if "system" in cmd: + return mock_system_info_response + elif "device-certificate" in cmd: + return mock_device_cert_response + + monkeypatch.setattr(firewall, "op", mock_op) + + collected_device = collect_data_from_firewall(firewall) + + assert collected_device.device_name == device.device_name + assert collected_device.model == device.model + assert collected_device.device_certificate == device.device_certificate + assert collected_device.device_certificate_expiry_date == device.device_certificate_expiry_date \ No newline at end of file diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 0000000..1d42eec --- /dev/null +++ b/tests/test_device.py @@ -0,0 +1,16 @@ +# tests/test_device.py + +from tests.factories import DeviceInfoFactory + +def test_device_info_creation(): + device = DeviceInfoFactory() + + assert device.device_name == "device5" + assert device.model == "PA-220" + assert device.serial_number == "serial5" + assert device.ipv4_address == "192.168.1.5" + assert device.device_state == "Connected" + assert device.device_certificate == "Valid" + assert device.device_certificate_expiry_date == "2024-12-31" + assert device.software_version == "10.0.0" + assert device.globalprotect_client == "5.2.6" \ No newline at end of file diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 0000000..bb65998 --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,32 @@ +# tests/test_filters.py + +from device_certificate_report.utilities.filters import ( + filter_devices_by_model, + split_devices_by_version, +) +from tests.factories import DeviceInfoFactory + +def test_filter_devices_by_model(): + device1 = DeviceInfoFactory(model="PA-220") + device2 = DeviceInfoFactory(device_name="device2", model="PA-460") + device3 = DeviceInfoFactory(device_name="device3", model="UnknownModel") + + devices = [device1, device2, device3] + affected, unaffected = filter_devices_by_model(devices) + assert len(affected) == 1 + assert len(unaffected) == 2 + assert affected[0].device_name == device1.device_name + assert unaffected[0].device_name == device2.device_name + assert unaffected[1].notes == "Model not recognized; considered unaffected." + +def test_split_devices_by_version(): + device1 = DeviceInfoFactory(software_version="9.1.10") + device2 = DeviceInfoFactory(device_name="device2", software_version="10.2.12-h12") + device3 = DeviceInfoFactory(device_name="device3", software_version="11.2.0") + + devices = [device1, device2, device3] + no_upgrade_required, upgrade_required = split_devices_by_version(devices) + assert len(no_upgrade_required) == 2 + assert len(upgrade_required) == 1 + assert upgrade_required[0].device_name == device1.device_name + assert upgrade_required[0].min_required_version == "9.1.11-h5" \ No newline at end of file diff --git a/tests/test_hardware_families.py b/tests/test_hardware_families.py new file mode 100644 index 0000000..b0c860c --- /dev/null +++ b/tests/test_hardware_families.py @@ -0,0 +1,18 @@ +# tests/test_hardware_families.py + +from device_certificate_report.utilities.filters import ( + is_affected_model, + is_unaffected_model, +) + + +def test_is_affected_model(): + assert is_affected_model("PA-220") + assert is_affected_model("PA-3020") + assert not is_affected_model("PA-460") + + +def test_is_unaffected_model(): + assert is_unaffected_model("PA-460") + assert is_unaffected_model("PA-1410") + assert not is_unaffected_model("PA-220") diff --git a/tests/test_panos_versions.py b/tests/test_panos_versions.py new file mode 100644 index 0000000..f91b5eb --- /dev/null +++ b/tests/test_panos_versions.py @@ -0,0 +1,27 @@ +# tests/test_panos_versions.py + +from device_certificate_report.utilities.filters import is_version_affected + + +def test_is_version_affected(): + affected, min_version = is_version_affected("9.1.10") + assert affected + assert min_version == "9.1.11-h5" + + +def test_is_version_not_affected(): + affected, min_version = is_version_affected("10.2.12-h6") + assert not affected + assert min_version == "" + + +def test_is_version_affected_with_globalprotect(): + affected, min_version = is_version_affected("10.2.2-h3", is_global_protect=True) + assert affected + assert min_version == "10.2.2-h5" + + +def test_is_version_not_affected_with_globalprotect(): + affected, min_version = is_version_affected("11.2.0", is_global_protect=True) + assert not affected + assert min_version == "" diff --git a/tests/test_pdf_generation.py b/tests/test_pdf_generation.py new file mode 100644 index 0000000..b64a081 --- /dev/null +++ b/tests/test_pdf_generation.py @@ -0,0 +1,32 @@ +# tests/test_pdf_generation.py + +from device_certificate_report.utilities.pdf_generation import generate_report +from tests.factories import DeviceInfoFactory +import os + +def test_generate_report(tmp_path): + device1 = DeviceInfoFactory() + device2 = DeviceInfoFactory( + device_name="device2", + model="PA-460", + software_version="10.1.0", + ) + + unaffected_devices = [device2] + no_upgrade_required = [] + upgrade_required = [device1] + devices_with_globalprotect = [] + devices_with_certificates = [] + + output_file = tmp_path / "report.pdf" + generate_report( + unaffected_devices=unaffected_devices, + no_upgrade_required=no_upgrade_required, + upgrade_required=upgrade_required, + devices_with_globalprotect=devices_with_globalprotect, + devices_with_certificates=devices_with_certificates, + output_file=str(output_file), + ) + + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 \ No newline at end of file diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..0d386f6 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,29 @@ +# tests/test_version.py + +import pytest +from device_certificate_report.components.version import parse_version, Version + +def test_parse_version(): + version_str = "10.2.3-h4" + version = parse_version(version_str) + assert version.major == 10 + assert version.feature == 2 + assert version.maintenance == 3 + assert version.hotfix == 4 + +def test_parse_version_no_hotfix(): + version_str = "9.1.13" + version = parse_version(version_str) + assert version.major == 9 + assert version.feature == 1 + assert version.maintenance == 13 + assert version.hotfix == 0 + +def test_version_comparison(): + v1 = Version(10, 1, 6, 8) + v2 = Version(10, 1, 7, 1) + assert v1 < v2 + +def test_invalid_version(): + with pytest.raises(ValueError): + parse_version("invalid-version") \ No newline at end of file