Skip to content
4 changes: 3 additions & 1 deletion anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ def validate_regex(value: str) -> str:
SnmpErrorCounter = Literal[
"inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs"
]

IPv4RouteType = Literal[
"connected",
"static",
Expand Down Expand Up @@ -238,3 +237,6 @@ def validate_regex(value: str) -> str:
"Route Cache Route",
"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"]
35 changes: 35 additions & 0 deletions anta/input_models/snmp.py
Original file line number Diff line number Diff line change
@@ -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}"
87 changes: 86 additions & 1 deletion anta/tests/snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +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 field_validator

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_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.
Expand Down Expand Up @@ -339,3 +345,82 @@ 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).

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.

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.
* 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:
snmp_users:
- username: test
group_name: test_group
security_model: v3
authentication_type: MD5
encryption: AES-128
```
"""

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."""

snmp_users: list[SnmpUser]
"""List of SNMP users."""

@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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we still using ; in other tests? I don't remember :D

raise ValueError(msg)
return snmp_users

@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifySnmpUser."""
self.result.is_success()

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

# Verify SNMP user details.
if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{security_model}.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}")

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 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}")
16 changes: 16 additions & 0 deletions docs/api/tests.snmp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__"]
8 changes: 8 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
165 changes: 165 additions & 0 deletions tests/units/anta_tests/test_snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
VerifySnmpLocation,
VerifySnmpPDUCounters,
VerifySnmpStatus,
VerifySnmpUser,
)
from tests.units.anta_tests import test

Expand Down Expand Up @@ -319,4 +320,168 @@
],
},
},
{
"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": {
"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"},
]
},
"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": {
"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"},
]
},
"expected": {
"result": "failure",
"messages": [
"User: Test1 Version: v1 - Not found",
"User: Test2 Version: v2c - Not found",
"User: Test4 Version: v3 - Not found",
],
},
},
{
"name": "failure-incorrect-group",
"test": VerifySnmpUser,
"eos_data": [
{
"usersByVersion": {
"v1": {
"users": {
"Test1": {
"groupName": "TestGroup2",
},
}
},
"v2c": {
"users": {
"Test2": {
"groupName": "TestGroup1",
},
}
},
"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": "TestGroup3",
"v3Params": {"authType": "SHA-512", "privType": "AES-192"},
},
"Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}},
}
},
}
}
],
"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"},
]
},
"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",
],
},
},
]
Loading