diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py index d408d9311..99dfd5154 100644 --- a/anta/input_models/snmp.py +++ b/anta/input_models/snmp.py @@ -6,6 +6,7 @@ from __future__ import annotations from ipaddress import IPv4Address +from typing import Literal from pydantic import BaseModel, ConfigDict @@ -72,3 +73,29 @@ def __str__(self) -> str: - Source Interface: Ethernet1 VRF: default """ return f"Source Interface: {self.interface} VRF: {self.vrf}" + + +class SnmpGroup(BaseModel): + """Model for a SNMP group.""" + + group_name: str + """SNMP group name.""" + version: SnmpVersion + """SNMP protocol version.""" + read_view: str | None = None + """Optional field, View to restrict read access.""" + write_view: str | None = None + """Optional field, View to restrict write access.""" + notify_view: str | None = None + """Optional field, View to restrict notifications.""" + authentication: Literal["v3Auth", "v3Priv", "v3NoAuth"] | None = None + """SNMPv3 authentication settings. Required when version is v3.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the SnmpGroup for reporting. + + Examples + -------- + - Group: Test_Group Version: v2c + """ + return f"Group: {self.group_name}, Version: {self.version}" diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 84c5470e6..95daed0e8 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -12,7 +12,7 @@ from pydantic import field_validator from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu -from anta.input_models.snmp import SnmpHost, SnmpSourceInterface, SnmpUser +from anta.input_models.snmp import SnmpGroup, SnmpHost, SnmpSourceInterface, SnmpUser from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -543,3 +543,81 @@ def test(self) -> None: self.result.is_failure(f"{interface_details} - Not configured") elif actual_interface != interface_details.interface: self.result.is_failure(f"{interface_details} - Incorrect source interface - Actual: {actual_interface}") + + +class VerifySnmpGroup(AntaTest): + """Verifies the SNMP group configurations for specified version(s). + + This test performs the following checks: + + 1. Verifies that the SNMP group is configured for the specified version. + 2. For SNMP version 3, verify that the security model matches the expected value. + 3. Ensures that SNMP group configurations, including read, write, and notify views, align with version-specific requirements. + + Expected Results + ---------------- + * Success: The test will pass if the provided SNMP group and all specified parameters are correctly configured. + * Failure: The test will fail if the provided SNMP group is not configured or specified parameters are not correctly configured. + + Examples + -------- + ```yaml + anta.tests.snmp: + - VerifySnmpGroup: + snmp_groups: + - group_name: Group1 + version: v1 + read_view: group_read_1 + write_view: group_write_1 + notify_view: group_notify_1 + ``` + """ + + categories: ClassVar[list[str]] = ["snmp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp group", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifySnmpGroup test.""" + + snmp_groups: list[SnmpGroup] + """List of SNMP groups.""" + + @field_validator("snmp_groups") + @classmethod + def validate_snmp_groups(cls, snmp_groups: list[SnmpGroup]) -> list[SnmpGroup]: + """Validate the inputs provided to the SnmpGroup class.""" + for snmp_group in snmp_groups: + if snmp_group.version == "v3" and snmp_group.authentication is None: + msg = f"{snmp_group}; `authentication` field is required for `version: v3`" + raise ValueError(msg) + return snmp_groups + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySnmpGroup.""" + self.result.is_success() + for group in self.inputs.snmp_groups: + # Verify SNMP group details. + if not (group_details := get_value(self.instance_commands[0].json_output, f"groups.{group.group_name}.versions.{group.version}")): + self.result.is_failure(f"{group} - Not configured") + continue + + # Verify SNMP views, the read, write and notify settings aligning with version-specific requirements. + for view_type in ["read", "write", "notify"]: + expected_view = getattr(group, f"{view_type}_view") + + # Verify actual view is configured. + if expected_view and group_details.get(f"{view_type}View") == "": + self.result.is_failure(f"{group} View: {view_type} - Not configured") + elif expected_view and not all( + [(act_view := group_details.get(f"{view_type}View")) == expected_view, (view_configured := group_details.get(f"{view_type}ViewConfig"))] + ): + self.result.is_failure( + f"{group} {view_type.title()} View: {expected_view} - " + f"View configuration mismatch - {view_type.title()} View: {act_view}, " + f"Configured: {view_configured}" + ) + + # For version v3, verify that the security model aligns with the expected value. + if group.version == "v3" and (actual_auth := group_details.get("secModel")) != group.authentication: + self.result.is_failure(f"{group} - Incorrect security model - Expected: {group.authentication} Actual: {actual_auth}") diff --git a/examples/tests.yaml b/examples/tests.yaml index 6c64f5d9f..c8fb5672a 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -770,6 +770,14 @@ anta.tests.snmp: # Verifies the SNMP error counters. error_counters: - inVersionErrs + - VerifySnmpGroup: + # Verifies the SNMP group configurations for specified version(s). + snmp_groups: + - group_name: Group1 + version: v1 + read_view: group_read_1 + write_view: group_write_1 + notify_view: group_notify_1 - VerifySnmpHostLogging: # Verifies SNMP logging configurations. hosts: diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index fc30ad6ce..94e1c28cd 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -10,6 +10,7 @@ from anta.tests.snmp import ( VerifySnmpContact, VerifySnmpErrorCounters, + VerifySnmpGroup, VerifySnmpHostLogging, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, @@ -580,4 +581,326 @@ ], }, }, + { + "name": "success", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read_1", + "readViewConfig": True, + "writeView": "group_write_1", + "writeViewConfig": True, + "notifyView": "group_notify_1", + "notifyViewConfig": True, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read_2", + "readViewConfig": True, + "writeView": "group_write_2", + "writeViewConfig": True, + "notifyView": "group_notify_2", + "notifyViewConfig": True, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3Auth", + "readView": "group_read_3", + "readViewConfig": True, + "writeView": "group_write_3", + "writeViewConfig": True, + "notifyView": "group_notify_3", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "write_view": "group_write_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "v3Auth", + }, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-incorrect-view", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3Auth", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "write_view": "group_write_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "v3Auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1 Read View: group_read_1 - View configuration mismatch - Read View: group_read, Configured: True", + "Group: Group1, Version: v1 Write View: group_write_1 - View configuration mismatch - Write View: group_write, Configured: True", + "Group: Group1, Version: v1 Notify View: group_notify_1 - View configuration mismatch - Notify View: group_notify, Configured: True", + "Group: Group2, Version: v2c Read View: group_read_2 - View configuration mismatch - Read View: group_read, Configured: True", + "Group: Group2, Version: v2c Write View: group_write_2 - View configuration mismatch - Write View: group_write, Configured: True", + "Group: Group2, Version: v2c Notify View: group_notify_2 - View configuration mismatch - Notify View: group_notify, Configured: True", + "Group: Group3, Version: v3 Read View: group_read_3 - View configuration mismatch - Read View: group_read, Configured: True", + "Group: Group3, Version: v3 Write View: group_write_3 - View configuration mismatch - Write View: group_write, Configured: True", + "Group: Group3, Version: v3 Notify View: group_notify_3 - View configuration mismatch - Notify View: group_notify, Configured: True", + ], + }, + }, + { + "name": "failure-view-config-not-found", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3Auth", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read", + "write_view": "group_write", + "notify_view": "group_notify", + "authentication": "v3Auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1 Read View: group_read - View configuration mismatch - Read View: group_read, Configured: False", + "Group: Group1, Version: v1 Write View: group_write - View configuration mismatch - Write View: group_write, Configured: False", + "Group: Group1, Version: v1 Notify View: group_notify - View configuration mismatch - Notify View: group_notify, Configured: False", + "Group: Group2, Version: v2c Read View: group_read - View configuration mismatch - Read View: group_read, Configured: False", + "Group: Group2, Version: v2c Write View: group_write - View configuration mismatch - Write View: group_write, Configured: False", + "Group: Group2, Version: v2c Notify View: group_notify - View configuration mismatch - Notify View: group_notify, Configured: False", + "Group: Group3, Version: v3 Read View: group_read - View configuration mismatch - Read View: group_read, Configured: False", + "Group: Group3, Version: v3 Write View: group_write - View configuration mismatch - Write View: group_write, Configured: False", + "Group: Group3, Version: v3 Notify View: group_notify - View configuration mismatch - Notify View: group_notify, Configured: False", + ], + }, + }, + { + "name": "failure-group-version-not-configured", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": {"versions": {"v1": {}}}, + "Group2": {"versions": {"v2c": {}}}, + "Group3": {"versions": {"v3": {}}}, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "write_view": "group_write_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "v3Auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1 - Not configured", + "Group: Group2, Version: v2c - Not configured", + "Group: Group3, Version: v3 - Not configured", + ], + }, + }, + { + "name": "failure-incorrect-v3-auth-model", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group3": { + "versions": { + "v3": { + "secModel": "v3Priv", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read", + "write_view": "group_write", + "notify_view": "group_notify", + "authentication": "v3Auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group3, Version: v3 - Incorrect security model - Expected: v3Auth Actual: v3Priv", + ], + }, + }, + { + "name": "failure-view-not-configured", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group3": {"versions": {"v3": {"secModel": "v3Auth", "readView": "group_read", "readViewConfig": True, "writeView": "", "notifyView": ""}}}, + } + } + ], + "inputs": { + "snmp_groups": [ + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read", + "write_view": "group_write", + "authentication": "v3Auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group3, Version: v3 View: write - Not configured", + ], + }, + }, ] diff --git a/tests/units/input_models/test_snmp.py b/tests/units/input_models/test_snmp.py index 94551ca76..506f23ac9 100644 --- a/tests/units/input_models/test_snmp.py +++ b/tests/units/input_models/test_snmp.py @@ -11,10 +11,10 @@ import pytest from pydantic import ValidationError -from anta.tests.snmp import VerifySnmpUser +from anta.tests.snmp import VerifySnmpGroup, VerifySnmpUser if TYPE_CHECKING: - from anta.input_models.snmp import SnmpUser + from anta.input_models.snmp import SnmpGroup, SnmpUser class TestVerifySnmpUserInput: @@ -42,3 +42,55 @@ def test_invalid(self, snmp_users: list[SnmpUser]) -> None: """Test VerifySnmpUser.Input invalid inputs.""" with pytest.raises(ValidationError): VerifySnmpUser.Input(snmp_users=snmp_users) + + +class TestVerifySnmpGroupInput: + """Test anta.tests.snmp.VerifySnmpGroup.Input.""" + + @pytest.mark.parametrize( + ("snmp_groups"), + [ + pytest.param( + [{"group_name": "abc", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}], id="valid-v1" + ), + pytest.param( + [ + { + "group_name": "abc", + "version": "v3", + "read_view": "group_read_1", + "write_view": "group_write_1", + "notify_view": "group_notify_1", + "authentication": "v3Auth", + } + ], + id="valid-v3", + ), + ], + ) + def test_valid(self, snmp_groups: list[SnmpGroup]) -> None: + """Test VerifySnmpGroup.Input valid inputs.""" + VerifySnmpGroup.Input(snmp_groups=snmp_groups) + + @pytest.mark.parametrize( + ("snmp_groups"), + [ + pytest.param( + [ + { + "group_name": "abc", + "version": "v3", + "read_view": "group_read_1", + "write_view": "group_write_1", + "notify_view": "group_notify_1", + "authentication": None, + } + ], + id="invalid-v3", + ), + ], + ) + def test_invalid(self, snmp_groups: list[SnmpGroup]) -> None: + """Test VerifySnmpGroup.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifySnmpGroup.Input(snmp_groups=snmp_groups)