diff --git a/anta/custom_types.py b/anta/custom_types.py index c29811826..460e3e638 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -208,3 +208,6 @@ def validate_regex(value: str) -> str: SnmpErrorCounter = Literal[ "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" ] +SnmpVersion = Literal["v1", "v2c", "v3"] +HashingAlgorithms = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"] +EncryptionAlgorithms = Literal["AES-128", "AES-192", "AES-256", "DES"] diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 217e32059..58d27b92a 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, ClassVar, get_args -from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu +from pydantic import BaseModel, model_validator + +from anta.custom_types import EncryptionAlgorithms, HashingAlgorithms, PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion from anta.models import AntaCommand, AntaTest -from anta.tools import get_value +from anta.tools import get_failed_logs, get_value if TYPE_CHECKING: from anta.models import AntaTemplate @@ -350,3 +352,103 @@ 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 VerifySnmpUser(AntaTest): + """Verifies the SNMP user configurations for specified version(s). + + - Verifies that the valid user name and group name. + - Ensures that the SNMP v3 security model, the user authentication and privacy settings aligning with version-specific requirements. + + Expected Results + ---------------- + * Success: The test will pass if the provided SNMP user and all specified parameters are correctly configured. + * Failure: The test will fail if the provided SNMP user is not configured or specified parameters are not correctly configured. + + Examples + -------- + ```yaml + anta.tests.snmp: + - VerifySnmpUser: + users: + - username: test + group_name: test_group + security_model: v3 + authentication_type: MD5 + priv_type: AES-128 + ``` + """ + + name = "VerifySnmpUser" + description = "Verifies the SNMP user configurations for specified version(s)." + categories: ClassVar[list[str]] = ["snmp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp user", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifySnmpUser test.""" + + users: list[SnmpUser] + """List of SNMP users.""" + + class SnmpUser(BaseModel): + """Model for a SNMP User.""" + + username: str + """SNMP user name.""" + group_name: str + """SNMP group for the user.""" + security_model: SnmpVersion + """SNMP protocol version..""" + authentication_type: HashingAlgorithms | None = None + """User authentication settings.""" + priv_type: EncryptionAlgorithms | None = None + """User privacy settings.""" + + @model_validator(mode="after") + def validate_inputs(self: BaseModel) -> BaseModel: + """Validate the inputs provided to the SnmpUser class.""" + if self.security_model in ["v1", "v2c"] and (self.authentication_type or self.priv_type) is not None: + msg = "SNMP versions 1 and 2c, do not support encryption or advanced authentication." + raise ValueError(msg) + return self + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySnmpUser.""" + self.result.is_success() + failures: str = "" + + for user in self.inputs.users: + username = user.username + group_name = user.group_name + security_model = user.security_model + authentication_type = user.authentication_type + priv_type = user.priv_type + + # Verify SNMP host details. + if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{security_model}.users.{username}")): + failures += f"SNMP user '{username}' is not configured with security model '{security_model}'.\n" + continue + + # Update expected host details. + expected_user_details = {"user group": group_name} + + # Update actual host details. + actual_user_details = {"user group": user_details.get("groupName", "Not Found")} + + if authentication_type: + expected_user_details["authentication type"] = authentication_type + actual_user_details["authentication type"] = user_details.get("v3Params", {}).get("authType", "Not Found") + + if priv_type: + expected_user_details["privacy type"] = priv_type + actual_user_details["privacy type"] = user_details.get("v3Params", {}).get("privType", "Not Found") + + # Collecting failures logs if any. + failure_logs = get_failed_logs(expected_user_details, actual_user_details) + if failure_logs: + failures += f"For SNMP user {username}:{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..7559348cf 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -405,6 +405,17 @@ anta.tests.snmp: error_counters: - inVersionErrs - inBadCommunityNames + - VerifySnmpUser: + users: + - username: Test1 + group_name: TestGroup1 + security_model: v3 + authentication_type: MD5 + priv_type: AES-128 + - username: Test2 + group_name: TestGroup2 + security_model: v3 + authentication_type: SHA-256 anta.tests.software: - VerifyEOSVersion: diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index e7d8da8ba..d9ec4f012 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -15,6 +15,7 @@ VerifySnmpLocation, VerifySnmpPDUCounters, VerifySnmpStatus, + VerifySnmpUser, ) from tests.units.anta_tests import test @@ -319,4 +320,136 @@ ], }, }, + { + "name": "success", + "test": VerifySnmpUser, + "eos_data": [ + { + "usersByVersion": { + "v1": { + "users": { + "Test1": { + "groupName": "TestGroup1", + }, + } + }, + "v2c": { + "users": { + "Test2": { + "groupName": "TestGroup2", + }, + } + }, + "v3": { + "users": { + "Test3": { + "groupName": "TestGroup3", + "v3Params": {"authType": "SHA-384", "privType": "AES-128"}, + }, + "Test4": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-512", "privType": "AES-192"}}, + } + }, + } + } + ], + "inputs": { + "users": [ + {"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-not-configured", + "test": VerifySnmpUser, + "eos_data": [ + { + "usersByVersion": { + "v3": { + "users": { + "Test3": { + "groupName": "TestGroup3", + "v3Params": {"authType": "SHA-384", "privType": "AES-128"}, + }, + } + }, + } + } + ], + "inputs": { + "users": [ + {"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "SNMP user 'Test1' is not configured with security model 'v1'.\n" + "SNMP user 'Test2' is not configured with security model 'v2c'.\n" + "SNMP user 'Test4' is not configured with security model 'v3'." + ], + }, + }, + { + "name": "failure-incorrect-configure", + "test": VerifySnmpUser, + "eos_data": [ + { + "usersByVersion": { + "v1": { + "users": { + "Test1": { + "groupName": "TestGroup2", + }, + } + }, + "v2c": { + "users": { + "Test2": { + "groupName": "TestGroup1", + }, + } + }, + "v3": { + "users": { + "Test3": { + "groupName": "TestGroup4", + "v3Params": {"authType": "SHA-512", "privType": "AES-192"}, + }, + "Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}}, + } + }, + } + } + ], + "inputs": { + "users": [ + {"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "For SNMP user Test1:\nExpected `TestGroup1` as the user group, but found `TestGroup2` instead.\n" + "For SNMP user Test2:\nExpected `TestGroup2` as the user group, but found `TestGroup1` instead.\n" + "For SNMP user Test3:\n" + "Expected `TestGroup3` as the user group, but found `TestGroup4` instead.\n" + "Expected `SHA-384` as the authentication type, but found `SHA-512` instead.\n" + "Expected `AES-128` as the privacy type, but found `AES-192` instead.\n" + "For SNMP user Test4:\n" + "Expected `TestGroup3` as the user group, but found `TestGroup4` instead.\n" + "Expected `SHA-512` as the authentication type, but found `SHA-384` instead.\n" + "Expected `AES-192` as the privacy type, but found `AES-128` instead." + ], + }, + }, ]