diff --git a/anta/custom_types.py b/anta/custom_types.py index c29811826..faf4eb058 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -208,3 +208,4 @@ def validate_regex(value: str) -> str: SnmpErrorCounter = Literal[ "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" ] +SnmpVersion = Literal["v1", "v2c", "v3"] diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 217e32059..e35ce3eab 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -7,9 +7,11 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, get_args +from typing import TYPE_CHECKING, Any, ClassVar, Literal, get_args -from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu +from pydantic import BaseModel, model_validator + +from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -17,6 +19,67 @@ from anta.models import AntaTemplate +def _get_snmp_group_failures( + version: SnmpVersion, + read_view: str | None, + write_view: str | None, + notify_view: str | None, + authentication: Literal["v3Auth", "v3Priv", "v3NoAuth"] | None, + group_details: dict[str, Any], +) -> str: + """ + Validate SNMP group configurations and return failure messages if issues are found. + + Parameters + ---------- + version + SNMP protocol version. + read_view + View to restrict read access. + write_view + View to restrict write access. + notify_view + View to restrict notifications. + authentication + Advanced authentication in v3 SNMP version + group_details + The SNMP group output from device. + + Returns + ------- + str + Failed log of a group details. + + """ + failure: str = "" + + def check_view(view_name: str, expected_view: str, default_message: str) -> str: + """Check actual view and return failure log if any.""" + failure_log: str = "" + actual_view = "Not Found" if (view := group_details.get(view_name)) is None else view or default_message + config_view = group_details.get(f"{view_name}Config") + + if not config_view: + failure_log += f"\nThe '{expected_view}' view is not configured." + if expected_view != actual_view: + failure_log += f"\nExpected '{expected_view}' as '{view_name}' but found '{actual_view}' instead." + return failure_log + + # Check views (read, write, notify) + if read_view: + failure += check_view("readView", read_view, "default: all included") + if write_view: + failure += check_view("writeView", write_view, "no write view specified") + if notify_view: + failure += check_view("notifyView", notify_view, "no notify view specified") + + # Check version-specific authentication + if version == "v3" and (actual_auth := group_details.get("secModel")) != authentication: + failure += f"\nExpected '{authentication}' as security model but found '{actual_auth}' instead." + + return failure + + class VerifySnmpStatus(AntaTest): """Verifies whether the SNMP agent is enabled in a specified VRF. @@ -350,3 +413,96 @@ def test(self) -> None: self.result.is_success() else: self.result.is_failure(f"The following SNMP error counters are not found or have non-zero error counters:\n{error_counters_not_ok}") + + +class VerifySnmpGroup(AntaTest): + """Verifies the SNMP group configurations for specified version(s). + + - Verifies that the valid group name and security model version. + - Ensures that the SNMP views, the read, write and notify settings aligning 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 + ``` + """ + + name = "VerifySnmpGroup" + description = "Verifies the SNMP group configurations for specified version(s)." + 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.""" + + class SnmpGroup(BaseModel): + """Model for a SNMP group.""" + + group_name: str + """SNMP group for the user.""" + version: SnmpVersion + """SNMP protocol version.""" + read_view: str | None = None + """View to restrict read access.""" + write_view: str | None = None + """View to restrict write access.""" + notify_view: str | None = None + """View to restrict notifications.""" + authentication: Literal["v3Auth", "v3Priv", "v3NoAuth"] | None = None + """Advanced authentication in v3 SNMP version. Defaults to None. + - v3Auth: Group using authentication but not privacy + - v3Priv: Group using both authentication and privacy + - v3NoAuth: Group using neither authentication nor privacy + """ + + @model_validator(mode="after") + def validate_inputs(self: BaseModel) -> BaseModel: + """Validate the inputs provided to the SnmpGroup class.""" + if self.version == "v3" and self.authentication is None: + msg = "SNMP versions v3, advanced authentication is required." + raise ValueError(msg) + return self + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySnmpGroup.""" + self.result.is_success() + failures: str = "" + + for group in self.inputs.snmp_groups: + group_name = group.group_name + version = group.version + read_view = group.read_view + write_view = group.write_view + notify_view = group.notify_view + authentication = group.authentication + + # Verify SNMP group details. + if not (group_details := get_value(self.instance_commands[0].json_output, f"groups.{group_name}.versions.{version}")): + failures += f"SNMP group '{group_name}' is not configured with security model '{version}'.\n" + continue + + # Collecting failures logs if any. + failure_logs = _get_snmp_group_failures(version, read_view, write_view, notify_view, authentication, group_details) + if failure_logs: + failures += f"For SNMP group {group_name} with SNMP version {version}:{failure_logs}\n" + + # Check if there are any failures. + if failures: + self.result.is_failure(failures) diff --git a/examples/tests.yaml b/examples/tests.yaml index d8f3332ae..7aefe0a96 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -405,6 +405,24 @@ anta.tests.snmp: error_counters: - inVersionErrs - inBadCommunityNames + - VerifySnmpGroup: + 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 + authentication: v3Auth + read_view: group_read_3 + write_view: group_write_3 + notify_view: group_notify_3 anta.tests.software: - VerifyEOSVersion: diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index e7d8da8ba..c37c9bcca 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, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, VerifySnmpLocation, @@ -319,4 +320,264 @@ ], }, }, + { + "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": "v3Priv", + "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": [ + "For SNMP group Group1 with SNMP version v1:\n" + "Expected 'group_read_1' as 'readView' but found 'group_read' instead.\n" + "Expected 'group_write_1' as 'writeView' but found 'group_write' instead.\n" + "Expected 'group_notify_1' as 'notifyView' but found 'group_notify' instead.\n" + "For SNMP group Group2 with SNMP version v2c:\n" + "Expected 'group_read_2' as 'readView' but found 'group_read' instead.\n" + "Expected 'group_write_2' as 'writeView' but found 'group_write' instead.\n" + "Expected 'group_notify_2' as 'notifyView' but found 'group_notify' instead.\n" + "For SNMP group Group3 with SNMP version v3:\n" + "Expected 'group_read_3' as 'readView' but found 'group_read' instead.\n" + "Expected 'group_write_3' as 'writeView' but found 'group_write' instead.\n" + "Expected 'group_notify_3' as 'notifyView' but found 'group_notify' instead.\n" + "Expected 'v3Auth' as security model but found 'v3Priv' instead.\n" + ], + }, + }, + { + "name": "failure-view-not-configured", + "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_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": [ + "For SNMP group Group1 with SNMP version v1:\n" + "The 'group_read_1' view is not configured.\n" + "The 'group_write_1' view is not configured.\n" + "The 'group_notify_1' view is not configured.\n" + "For SNMP group Group2 with SNMP version v2c:\n" + "The 'group_read_2' view is not configured.\n" + "The 'group_write_2' view is not configured.\n" + "The 'group_notify_2' view is not configured.\n" + "For SNMP group Group3 with SNMP version v3:\n" + "The 'group_read_3' view is not configured.\n" + "The 'group_write_3' view is not configured.\n" + "The 'group_notify_3' view is not configured.\n" + ], + }, + }, + { + "name": "failure-view-not-found", + "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": [ + "SNMP group 'Group1' is not configured with security model 'v1'.\n" + "SNMP group 'Group2' is not configured with security model 'v2c'.\n" + "SNMP group 'Group3' is not configured with security model 'v3'.\n" + ], + }, + }, ]