diff --git a/anta/custom_types.py b/anta/custom_types.py index d6790ed18..9606a3c93 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -208,12 +208,6 @@ def validate_regex(value: str) -> str: SnmpErrorCounter = Literal[ "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" ] -<<<<<<< HEAD -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"] -======= - IPv4RouteType = Literal[ "connected", "static", @@ -243,4 +237,6 @@ def validate_regex(value: str) -> str: "Route Cache Route", "CBF Leaked Route", ] ->>>>>>> main +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/input_models/snmp.py b/anta/input_models/snmp.py new file mode 100644 index 000000000..c179576b7 --- /dev/null +++ b/anta/input_models/snmp.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023-2025 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. +"""Module containing input models for SNMP tests.""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import EncryptionAlgorithms, HashingAlgorithms, SnmpVersion + + +class SnmpUser(BaseModel): + """Model for a SNMP User.""" + + model_config = ConfigDict(extra="forbid") + username: str + """SNMP user name.""" + group_name: str | None = None + """SNMP group for the user. Required field in the `VerifySnmpUser` test.""" + security_model: SnmpVersion | None = None + """SNMP protocol version. Required field in the `VerifySnmpUser` test.""" + authentication_type: HashingAlgorithms | None = None + """User authentication settings. Can be provided in the `VerifySnmpUser` test.""" + encryption: EncryptionAlgorithms | None = None + """User privacy settings. Can be provided in the `VerifySnmpUser` test.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the SnmpUser for reporting. + + Examples + -------- + User: Test Group: Test_Group Version: v2c + """ + return f"User: {self.username} Version: {self.security_model}" diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 57dfa3dda..9acff9798 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -7,17 +7,21 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, get_args +from typing import TYPE_CHECKING, ClassVar, TypeVar, get_args -from pydantic import BaseModel, model_validator +from pydantic import field_validator -from anta.custom_types import EncryptionAlgorithms, HashingAlgorithms, PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion +from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu +from anta.input_models.snmp import SnmpUser from anta.models import AntaCommand, AntaTest -from anta.tools import get_failed_logs, get_value +from anta.tools import get_value if TYPE_CHECKING: from anta.models import AntaTemplate +# Using a TypeVar for the SnmpUser model since mypy thinks it's a ClassVar and not a valid type when used in field validators +T = TypeVar("T", bound=SnmpUser) + class VerifySnmpStatus(AntaTest): """Verifies whether the SNMP agent is enabled in a specified VRF. @@ -346,98 +350,77 @@ def test(self) -> None: 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. + This test performs the following checks for each specified address family: + + 1. Verifies that the valid user name and group name. + 2. 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. + * Success: If all of the following conditions are met: + - All specified users are found in the SNMP configuration with valid user group. + - The SNMP v3 security model, the user authentication and privacy settings matches the required settings. + * Failure: If any of the following occur: + - A specified user is not found in the SNMP configuration. + - A user's group is not correct. + - For SNMP v3 security model, the user authentication and privacy settings does not matches the required settings. Examples -------- ```yaml anta.tests.snmp: - VerifySnmpUser: - users: + snmp_users: - username: test group_name: test_group security_model: v3 authentication_type: MD5 - priv_type: AES-128 + encryption: 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] + snmp_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." + @field_validator("snmp_users") + @classmethod + def validate_snmp_user(cls, snmp_users: list[T]) -> list[T]: + """Validate that 'authentication_type' or 'encryption' field is provided in each SNMP user.""" + for user in snmp_users: + if user.security_model == "v3" and not (user.authentication_type or user.encryption): + msg = f"{user}; At least one of 'authentication_type' or 'encryption' must be provided." raise ValueError(msg) - return self + return snmp_users @AntaTest.anta_test def test(self) -> None: """Main test function for VerifySnmpUser.""" self.result.is_success() - failures: str = "" - for user in self.inputs.users: + for user in self.inputs.snmp_users: username = user.username group_name = user.group_name security_model = user.security_model authentication_type = user.authentication_type - priv_type = user.priv_type + encryption = user.encryption - # Verify SNMP host details. + # Verify SNMP user 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" + self.result.is_failure(f"{user} - Not found") 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") + if group_name != (act_group := user_details.get("groupName", "Not Found")): + self.result.is_failure(f"{user} - Incorrect user group - Expected: {group_name} Actual: {act_group}") - # 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" + if security_model == "v3": + if authentication_type and (act_auth_type := user_details.get("v3Params", {}).get("authType", "Not Found")) != authentication_type: + self.result.is_failure(f"{user} - Incorrect authentication type - Expected: {authentication_type} Actual: {act_auth_type}") - # Check if there are any failures. - if failures: - self.result.is_failure(failures) + if encryption and (act_encryption := user_details.get("v3Params", {}).get("privType", "Not Found")) != encryption: + self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {encryption} Actual: {act_encryption}") diff --git a/examples/tests.yaml b/examples/tests.yaml index a4bc1fabf..844027ca8 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -743,6 +743,14 @@ anta.tests.snmp: - VerifySnmpStatus: # Verifies if the SNMP agent is enabled. vrf: default + - VerifySnmpUser: + # Verifies the SNMP user configurations for specified version(s). + snmp_users: + - username: test + group_name: test_group + security_model: v3 + authentication_type: MD5 + encryption: AES-128 anta.tests.software: - VerifyEOSExtensions: # Verifies that all EOS extensions installed on the device are enabled for boot persistence. diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index 1e03df7b7..581a2c5be 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -353,11 +353,11 @@ } ], "inputs": { - "users": [ + "snmp_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"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"}, ] }, "expected": {"result": "success"}, @@ -380,24 +380,24 @@ } ], "inputs": { - "users": [ + "snmp_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"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "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'." + "User: Test1 Version: v1 - Not found", + "User: Test2 Version: v2c - Not found", + "User: Test4 Version: v3 - Not found", ], }, }, { - "name": "failure-incorrect-configure", + "name": "failure-incorrect-group", "test": VerifySnmpUser, "eos_data": [ { @@ -416,10 +416,48 @@ }, } }, + "v3": {}, + } + } + ], + "inputs": { + "snmp_users": [ + {"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "User: Test1 Version: v1 - Incorrect user group - Expected: TestGroup1 Actual: TestGroup2", + "User: Test2 Version: v2c - Incorrect user group - Expected: TestGroup2 Actual: TestGroup1", + ], + }, + }, + { + "name": "failure-incorrect-auth-encryption", + "test": VerifySnmpUser, + "eos_data": [ + { + "usersByVersion": { + "v1": { + "users": { + "Test1": { + "groupName": "TestGroup1", + }, + } + }, + "v2c": { + "users": { + "Test2": { + "groupName": "TestGroup2", + }, + } + }, "v3": { "users": { "Test3": { - "groupName": "TestGroup4", + "groupName": "TestGroup3", "v3Params": {"authType": "SHA-512", "privType": "AES-192"}, }, "Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}}, @@ -429,26 +467,20 @@ } ], "inputs": { - "users": [ + "snmp_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"}, + {"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup4", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "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." + "User: Test3 Version: v3 - Incorrect authentication type - Expected: SHA-384 Actual: SHA-512", + "User: Test3 Version: v3 - Incorrect privacy type - Expected: AES-128 Actual: AES-192", + "User: Test4 Version: v3 - Incorrect authentication type - Expected: SHA-512 Actual: SHA-384", + "User: Test4 Version: v3 - Incorrect privacy type - Expected: AES-192 Actual: AES-128", ], }, },