From 111996182feeb6a25fd636515874f4c84b572c32 Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Fri, 11 Oct 2024 05:49:56 -0400 Subject: [PATCH 1/6] Added TC for SNMP user --- anta/custom_types.py | 3 + anta/tests/snmp.py | 106 +++++++++++++++++++++- examples/tests.yaml | 11 +++ tests/units/anta_tests/test_snmp.py | 133 ++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 2 deletions(-) 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." + ], + }, + }, ] From 581acc2964752328f3a88de23cd8a8482c072c4f Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 7 Jan 2025 22:13:44 -0500 Subject: [PATCH 2/6] Updated input model refactoring changes --- anta/custom_types.py | 10 +-- anta/input_models/snmp.py | 35 ++++++++++ anta/tests/snmp.py | 99 ++++++++++++----------------- examples/tests.yaml | 8 +++ tests/units/anta_tests/test_snmp.py | 80 ++++++++++++++++------- 5 files changed, 143 insertions(+), 89 deletions(-) create mode 100644 anta/input_models/snmp.py 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", ], }, }, From bb12cc2b36b6215a5ee8dcec73256f9994ffeced Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 7 Jan 2025 22:41:19 -0500 Subject: [PATCH 3/6] updated documentation apis --- anta/tests/snmp.py | 2 +- docs/api/tests.snmp.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 9acff9798..7e67b0d02 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -350,7 +350,7 @@ def test(self) -> None: class VerifySnmpUser(AntaTest): """Verifies the SNMP user configurations for specified version(s). - This test performs the following checks for each specified address family: + This test performs the following checks for each specified user: 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. diff --git a/docs/api/tests.snmp.md b/docs/api/tests.snmp.md index 85c147e2a..eec97202f 100644 --- a/docs/api/tests.snmp.md +++ b/docs/api/tests.snmp.md @@ -7,7 +7,10 @@ anta_title: ANTA catalog for SNMP tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.snmp + options: show_root_heading: false show_root_toc_entry: false @@ -18,3 +21,16 @@ anta_title: ANTA catalog for SNMP tests filters: - "!test" - "!render" + +# Input models + +::: anta.input_models.snmp + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + merge_init_into_class: false + anta_hide_test_module_description: true + show_labels: true + filters: ["!^__str__"] From 1f610eedfdda76c00dae0941fec57a38dbebb2de Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 7 Jan 2025 23:18:40 -0500 Subject: [PATCH 4/6] added unit tests for the input models --- anta/tests/snmp.py | 3 ++ tests/units/input_models/test_snmp.py | 46 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/units/input_models/test_snmp.py diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 7e67b0d02..e62129adb 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -393,6 +393,9 @@ class Input(AntaTest.Input): 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.group_name is None or user.security_model is None: + msg = f"{user} 'group_name' or 'security_model' field missing in the input" + raise ValueError(msg) 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) diff --git a/tests/units/input_models/test_snmp.py b/tests/units/input_models/test_snmp.py new file mode 100644 index 000000000..b26349ec3 --- /dev/null +++ b/tests/units/input_models/test_snmp.py @@ -0,0 +1,46 @@ +# 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. +"""Tests for anta.input_models.snmp.py.""" + +# pylint: disable=C0302 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from pydantic import ValidationError + +from anta.tests.snmp import VerifySnmpUser + +if TYPE_CHECKING: + from anta.input_models.snmp import SnmpUser + + +class TestVerifySnmpUserInput: + """Test anta.tests.snmp.VerifySnmpUser.Input.""" + + @pytest.mark.parametrize( + ("snmp_users"), + [ + pytest.param([{"username": "test", "group_name": "abc", "security_model": "v1", "authentication_type": None, "encryption": None}], id="valid-v1"), + pytest.param([{"username": "test", "group_name": "abc", "security_model": "v2c", "authentication_type": None, "encryption": None}], id="valid-v2c"), + pytest.param([{"username": "test", "group_name": "abc", "security_model": "v3", "authentication_type": "SHA", "encryption": "AES-128"}], id="valid-v3"), + ], + ) + def test_valid(self, snmp_users: list[SnmpUser]) -> None: + """Test VerifySnmpUser.Input valid inputs.""" + VerifySnmpUser.Input(snmp_users=snmp_users) + + @pytest.mark.parametrize( + ("snmp_users"), + [ + pytest.param([{"username": "test", "group_name": None, "security_model": "v1", "authentication_type": None, "encryption": None}], id="invalid-group"), + pytest.param([{"username": "test", "group_name": "abc", "security_model": None, "authentication_type": None, "encryption": None}], id="invalid-version"), + pytest.param([{"username": "test", "group_name": "abc", "security_model": "v3", "authentication_type": None, "encryption": None}], id="invalid-v3"), + ], + ) + def test_invalid(self, snmp_users: list[SnmpUser]) -> None: + """Test VerifySnmpUser.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifySnmpUser.Input(snmp_users=snmp_users) From 58e14db1645624d4739a128467e1fdea0952c5a0 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Wed, 8 Jan 2025 01:50:39 -0500 Subject: [PATCH 5/6] addressed review comments: updated docstrings, input model --- anta/custom_types.py | 4 +-- anta/input_models/snmp.py | 23 ++++++++----- anta/tests/snmp.py | 48 +++++++++++++-------------- examples/tests.yaml | 6 ++-- tests/units/anta_tests/test_snmp.py | 46 ++++++++++++------------- tests/units/input_models/test_snmp.py | 12 +++---- 6 files changed, 73 insertions(+), 66 deletions(-) diff --git a/anta/custom_types.py b/anta/custom_types.py index 9606a3c93..2308f07dd 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -238,5 +238,5 @@ def validate_regex(value: str) -> str: "CBF Leaked Route", ] 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"] +HashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"] +SnmpEncryptionAlgorithm = Literal["AES-128", "AES-192", "AES-256", "DES"] diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py index c179576b7..883bbcd33 100644 --- a/anta/input_models/snmp.py +++ b/anta/input_models/snmp.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict -from anta.custom_types import EncryptionAlgorithms, HashingAlgorithms, SnmpVersion +from anta.custom_types import HashingAlgorithm, SnmpEncryptionAlgorithm, SnmpVersion class SnmpUser(BaseModel): @@ -18,18 +18,25 @@ class SnmpUser(BaseModel): """SNMP user name.""" group_name: str | None = None """SNMP group for the user. Required field in the `VerifySnmpUser` test.""" - security_model: SnmpVersion | None = None + version: 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.""" + auth_type: HashingAlgorithm | None = None + """User authentication algorithm. Can be provided in the `VerifySnmpUser` test.""" + priv_type: SnmpEncryptionAlgorithm | None = None + """User privacy algorithm. 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 + - User: Test + - User: Test Group: Test_Group + - User: Test Group: Test_Group Version: v2c """ - return f"User: {self.username} Version: {self.security_model}" + base_string = f"User: {self.username}" + if self.group_name: + base_string += f" Group: {self.group_name}" + if self.version: + base_string += f" Version: {self.version}" + return base_string diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index e62129adb..6275ee96c 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -352,18 +352,18 @@ class VerifySnmpUser(AntaTest): This test performs the following checks for each specified user: - 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. + 1. Verifies that the user name and group name are valid. + 2. Ensures that the SNMP v3 security model, user authentication, and privacy settings align with version-specific requirements. Expected Results ---------------- * 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. + - All specified users are found in the SNMP configuration with valid user groups. + - The SNMP v3 security model, user authentication and privacy settings match 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. + - A user's group is incorrect. + - For SNMP v3 security model, the user authentication and privacy settings do not matches the required settings. Examples -------- @@ -373,9 +373,9 @@ class VerifySnmpUser(AntaTest): snmp_users: - username: test group_name: test_group - security_model: v3 - authentication_type: MD5 - encryption: AES-128 + version: v3 + auth_type: MD5 + priv_type: AES-128 ``` """ @@ -391,13 +391,13 @@ class Input(AntaTest.Input): @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.""" + """Validate that 'authentication_type' or 'priv_type' field is provided in each SNMP user.""" for user in snmp_users: - if user.group_name is None or user.security_model is None: - msg = f"{user} 'group_name' or 'security_model' field missing in the input" + if user.group_name is None or user.version is None: + msg = f"{user} 'group_name' or 'version' field missing in the input" raise ValueError(msg) - 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." + if user.version == "v3" and not (user.auth_type or user.priv_type): + msg = f"{user}; At least one of 'auth_type' or 'priv_type' must be provided." raise ValueError(msg) return snmp_users @@ -409,21 +409,21 @@ def test(self) -> None: 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 - encryption = user.encryption + version = user.version + auth_type = user.auth_type + priv_type = user.priv_type # Verify SNMP user details. - if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{security_model}.users.{username}")): + if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{version}.users.{username}")): self.result.is_failure(f"{user} - Not found") continue 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}") + self.result.is_failure(f"{user} - Incorrect user group - Actual: {act_group}") - 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}") + if version == "v3": + if auth_type and (act_auth_type := get_value(user_details, "v3Params.authType", "Not Found")) != auth_type: + self.result.is_failure(f"{user} - Incorrect authentication type - Expected: {auth_type} Actual: {act_auth_type}") - 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}") + if priv_type and (act_encryption := get_value(user_details, "v3Params.privType", "Not Found")) != priv_type: + self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {priv_type} Actual: {act_encryption}") diff --git a/examples/tests.yaml b/examples/tests.yaml index 844027ca8..a965a1ebc 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -748,9 +748,9 @@ anta.tests.snmp: snmp_users: - username: test group_name: test_group - security_model: v3 - authentication_type: MD5 - encryption: AES-128 + version: v3 + auth_type: MD5 + priv_type: 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 581a2c5be..d2eb6b87f 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -354,10 +354,10 @@ ], "inputs": { "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", "encryption": "AES-128"}, - {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"}, + {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "version": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"}, ] }, "expected": {"result": "success"}, @@ -381,18 +381,18 @@ ], "inputs": { "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", "encryption": "AES-128"}, - {"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"}, + {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "version": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"}, ] }, "expected": { "result": "failure", "messages": [ - "User: Test1 Version: v1 - Not found", - "User: Test2 Version: v2c - Not found", - "User: Test4 Version: v3 - Not found", + "User: Test1 Group: TestGroup1 Version: v1 - Not found", + "User: Test2 Group: TestGroup2 Version: v2c - Not found", + "User: Test4 Group: TestGroup3 Version: v3 - Not found", ], }, }, @@ -422,15 +422,15 @@ ], "inputs": { "snmp_users": [ - {"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"}, - {"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"}, + {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "version": "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", + "User: Test1 Group: TestGroup1 Version: v1 - Incorrect user group - Actual: TestGroup2", + "User: Test2 Group: TestGroup2 Version: v2c - Incorrect user group - Actual: TestGroup1", ], }, }, @@ -468,19 +468,19 @@ ], "inputs": { "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", "encryption": "AES-128"}, - {"username": "Test4", "group_name": "TestGroup4", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"}, + {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, + {"username": "Test2", "group_name": "TestGroup2", "version": "v2c"}, + {"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"}, + {"username": "Test4", "group_name": "TestGroup4", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"}, ] }, "expected": { "result": "failure", "messages": [ - "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", + "User: Test3 Group: TestGroup3 Version: v3 - Incorrect authentication type - Expected: SHA-384 Actual: SHA-512", + "User: Test3 Group: TestGroup3 Version: v3 - Incorrect privacy type - Expected: AES-128 Actual: AES-192", + "User: Test4 Group: TestGroup4 Version: v3 - Incorrect authentication type - Expected: SHA-512 Actual: SHA-384", + "User: Test4 Group: TestGroup4 Version: v3 - Incorrect privacy type - Expected: AES-192 Actual: AES-128", ], }, }, diff --git a/tests/units/input_models/test_snmp.py b/tests/units/input_models/test_snmp.py index b26349ec3..bbffe7152 100644 --- a/tests/units/input_models/test_snmp.py +++ b/tests/units/input_models/test_snmp.py @@ -23,9 +23,9 @@ class TestVerifySnmpUserInput: @pytest.mark.parametrize( ("snmp_users"), [ - pytest.param([{"username": "test", "group_name": "abc", "security_model": "v1", "authentication_type": None, "encryption": None}], id="valid-v1"), - pytest.param([{"username": "test", "group_name": "abc", "security_model": "v2c", "authentication_type": None, "encryption": None}], id="valid-v2c"), - pytest.param([{"username": "test", "group_name": "abc", "security_model": "v3", "authentication_type": "SHA", "encryption": "AES-128"}], id="valid-v3"), + pytest.param([{"username": "test", "group_name": "abc", "version": "v1", "auth_type": None, "priv_type": None}], id="valid-v1"), + pytest.param([{"username": "test", "group_name": "abc", "version": "v2c", "auth_type": None, "priv_type": None}], id="valid-v2c"), + pytest.param([{"username": "test", "group_name": "abc", "version": "v3", "auth_type": "SHA", "priv_type": "AES-128"}], id="valid-v3"), ], ) def test_valid(self, snmp_users: list[SnmpUser]) -> None: @@ -35,9 +35,9 @@ def test_valid(self, snmp_users: list[SnmpUser]) -> None: @pytest.mark.parametrize( ("snmp_users"), [ - pytest.param([{"username": "test", "group_name": None, "security_model": "v1", "authentication_type": None, "encryption": None}], id="invalid-group"), - pytest.param([{"username": "test", "group_name": "abc", "security_model": None, "authentication_type": None, "encryption": None}], id="invalid-version"), - pytest.param([{"username": "test", "group_name": "abc", "security_model": "v3", "authentication_type": None, "encryption": None}], id="invalid-v3"), + pytest.param([{"username": "test", "group_name": None, "version": "v1", "auth_type": None, "priv_type": None}], id="invalid-group"), + pytest.param([{"username": "test", "group_name": "abc", "version": None, "auth_type": None, "priv_type": None}], id="invalid-version"), + pytest.param([{"username": "test", "group_name": "abc", "version": "v3", "auth_type": None, "priv_type": None}], id="invalid-v3"), ], ) def test_invalid(self, snmp_users: list[SnmpUser]) -> None: From 96a6569b11f7366dc388e54d821ab8f6d4b8d688 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Wed, 8 Jan 2025 22:18:22 -0500 Subject: [PATCH 6/6] updated field validator --- anta/tests/snmp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 6275ee96c..d1d1dbeda 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -394,10 +394,10 @@ def validate_snmp_user(cls, snmp_users: list[T]) -> list[T]: """Validate that 'authentication_type' or 'priv_type' field is provided in each SNMP user.""" for user in snmp_users: if user.group_name is None or user.version is None: - msg = f"{user} 'group_name' or 'version' field missing in the input" + msg = f"{user}; 'group_name' or 'version' field missing in the input" raise ValueError(msg) if user.version == "v3" and not (user.auth_type or user.priv_type): - msg = f"{user}; At least one of 'auth_type' or 'priv_type' must be provided." + msg = f"{user}; 'auth_type' or 'priv_type' field is required with 'version: v3'" raise ValueError(msg) return snmp_users