Repository: aristanetworks/anta
Pull request #933 (2024-11-25 10:56:19)

fix(anta.tests): Cleaning up Security tests module (VerifySpecificIPSecConn)
# Description

Refactoring Security tests module to address the following issues:

- #809
- #587
- #388

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have run pre-commit for code linting and typing (`pre-commit run`)
- [X] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes (`tox -e testenv`)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [96.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933)


codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/aristanetworks/anta/branches/vitthalmagadum:feat/Security)

### Merging #933 will **not alter performance**

<sub>Comparing <code>vitthalmagadum:feat/Security</code> (ded70e0) with <code>main</code> (91eac73)</sub>



### Summary

`✅ 8` untouched benchmarks  




sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [96.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=933&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=933&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=933&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=933)



Patch:
diff --git a/anta/input_models/security.py b/anta/input_models/security.py
new file mode 100644
index 00000000..373d8973
--- /dev/null
+++ b/anta/input_models/security.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2023-2024 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 security tests."""
+
+from __future__ import annotations
+
+from ipaddress import IPv4Address
+from typing import Any
+from warnings import warn
+
+from pydantic import BaseModel, ConfigDict
+
+
+class IPSecPeer(BaseModel):
+    """IPSec (Internet Protocol Security) model represents the details of an IPv4 security peer."""
+
+    model_config = ConfigDict(extra="forbid")
+    peer: IPv4Address
+    """The IPv4 address of the security peer."""
+    vrf: str = "default"
+    """VRF context. Defaults to `default`."""
+    connections: list[IPSecConn] | None = None
+    """A list of IPv4 security connections associated with the peer. Defaults to None."""
+
+    def __str__(self) -> str:
+        """Return a string representation of the IPSecPeer model. Used in failure messages.
+
+        Examples
+        --------
+        - Peer: 1.1.1.1 VRF: default
+        """
+        return f"Peer: {self.peer} VRF: {self.vrf}"
+
+
+class IPSecConn(BaseModel):
+    """Details of an IPv4 security connection for a peer."""
+
+    model_config = ConfigDict(extra="forbid")
+    source_address: IPv4Address
+    """The IPv4 address of the source in the security connection."""
+    destination_address: IPv4Address
+    """The IPv4 address of the destination in the security connection."""
+
+
+class IPSecPeers(IPSecPeer):  # pragma: no cover
+    """Alias for the IPSecPeers model to maintain backward compatibility.
+
+    When initialized, it will emit a deprecation warning and call the IPSecPeer model.
+
+    TODO: Remove this class in ANTA v2.0.0.
+    """
+
+    def __init__(self, **data: Any) -> None:  # noqa: ANN401
+        """Initialize the IPSecPeer class, emitting a deprecation warning."""
+        warn(
+            message="IPSecPeers model is deprecated and will be removed in ANTA v2.0.0. Use the IPSecPeer model instead.",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
+        super().__init__(**data)


Test patch:
diff --git a/anta/tests/security.py b/anta/tests/security.py
index bbfa2b0e..38bf2409 100644
--- a/anta/tests/security.py
+++ b/anta/tests/security.py
@@ -8,12 +8,12 @@ from __future__ import annotations
 # Mypy does not understand AntaTest.Input typing
 # mypy: disable-error-code=attr-defined
 from datetime import datetime, timezone
-from ipaddress import IPv4Address
 from typing import TYPE_CHECKING, ClassVar, get_args
 
 from pydantic import BaseModel, Field, model_validator
 
 from anta.custom_types import EcdsaKeySize, EncryptionAlgorithm, PositiveInteger, RsaKeySize
+from anta.input_models.security import IPSecPeer, IPSecPeers
 from anta.models import AntaCommand, AntaTemplate, AntaTest
 from anta.tools import get_failed_logs, get_item, get_value
 
@@ -692,15 +692,22 @@ class VerifyIPSecConnHealth(AntaTest):
 
 
 class VerifySpecificIPSecConn(AntaTest):
-    """Verifies the state of IPv4 security connections for a specified peer.
+    """Verifies the IPv4 security connections.
 
-    It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.
-    If these addresses are not provided, it will verify all paths for the specified peer.
+    This test performs the following checks for each peer:
+
+      1. Validates that the VRF is configured.
+      2. Checks for the presence of IPv4 security connections for the specified peer.
+      3. For each relevant peer:
+        - If source and destination addresses are provided, verifies the security connection for the specific path exists and is `Established`.
+        - If no addresses are provided, verifies that all security connections associated with the peer are `Established`.
 
     Expected Results
     ----------------
-    * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.
-    * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.
+    * Success: If all checks pass for all specified IPv4 security connections.
+    * Failure: If any of the following occur:
+        - No IPv4 security connections are found for the peer
+        - The security connection is not established for the specified path or any of the peer connections is not established when no path is specified.
 
     Examples
     --------
@@ -719,35 +726,16 @@ class VerifySpecificIPSecConn(AntaTest):
     ```
     """
 
-    description = "Verifies IPv4 security connections for a peer."
     categories: ClassVar[list[str]] = ["security"]
-    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip security connection vrf {vrf} path peer {peer}")]
+    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip security connection vrf {vrf} path peer {peer}", revision=2)]
 
     class Input(AntaTest.Input):
         """Input model for the VerifySpecificIPSecConn test."""
 
-        ip_security_connections: list[IPSecPeers]
+        ip_security_connections: list[IPSecPeer]
         """List of IP4v security peers."""
-
-        class IPSecPeers(BaseModel):
-            """Details of IPv4 security peers."""
-
-            peer: IPv4Address
-            """IPv4 address of the peer."""
-
-            vrf: str = "default"
-            """Optional VRF for the IP security peer."""
-
-            connections: list[IPSecConn] | None = None
-            """Optional list of IPv4 security connections of a peer."""
-
-            class IPSecConn(BaseModel):
-                """Details of IPv4 security connections for a peer."""
-
-                source_address: IPv4Address
-                """Source IPv4 address of the connection."""
-                destination_address: IPv4Address
-                """Destination IPv4 address of the connection."""
+        IPSecPeers: ClassVar[type[IPSecPeers]] = IPSecPeers
+        """To maintain backward compatibility."""
 
     def render(self, template: AntaTemplate) -> list[AntaCommand]:
         """Render the template for each input IP Sec connection."""
@@ -757,15 +745,15 @@ class VerifySpecificIPSecConn(AntaTest):
     def test(self) -> None:
         """Main test function for VerifySpecificIPSecConn."""
         self.result.is_success()
+
         for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):
             conn_output = command_output.json_output["connections"]
-            peer = command_output.params.peer
-            vrf = command_output.params.vrf
             conn_input = input_peer.connections
+            vrf = input_peer.vrf
 
             # Check if IPv4 security connection is configured
             if not conn_output:
-                self.result.is_failure(f"No IPv4 security connection configured for peer `{peer}`.")
+                self.result.is_failure(f"{input_peer} - Not configured")
                 continue
 
             # If connection details are not provided then check all connections of a peer
@@ -775,10 +763,8 @@ class VerifySpecificIPSecConn(AntaTest):
                     if state != "Established":
                         source = conn_data.get("saddr")
                         destination = conn_data.get("daddr")
-                        vrf = conn_data.get("tunnelNs")
                         self.result.is_failure(
-                            f"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` "
-                            f"but found `{state}` instead."
+                            f"{input_peer} Source: {source} Destination: {destination} - Connection down - Expected: Established, Actual: {state}"
                         )
                 continue
 
@@ -794,14 +780,10 @@ class VerifySpecificIPSecConn(AntaTest):
                 if (source_input, destination_input, vrf) in existing_connections:
                     existing_state = existing_connections[(source_input, destination_input, vrf)]
                     if existing_state != "Established":
-                        self.result.is_failure(
-                            f"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` "
-                            f"for peer `{peer}` is `Established` but found `{existing_state}` instead."
-                        )
+                        failure = f"Expected: Established, Actual: {existing_state}"
+                        self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection down - {failure}")
                 else:
-                    self.result.is_failure(
-                        f"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found."
-                    )
+                    self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection not found.")
 
 
 class VerifyHardwareEntropy(AntaTest):
diff --git a/docs/api/tests.security.md b/docs/api/tests.security.md
index fe008ba6..59978323 100644
--- a/docs/api/tests.security.md
+++ b/docs/api/tests.security.md
@@ -7,7 +7,10 @@ anta_title: ANTA catalog for security tests
   ~ that can be found in the LICENSE file.
   -->
 
+# Tests
+
 ::: anta.tests.security
+
     options:
       show_root_heading: false
       show_root_toc_entry: false
@@ -18,3 +21,18 @@ anta_title: ANTA catalog for security tests
       filters:
         - "!test"
         - "!render"
+
+# Input models
+
+::: anta.input_models.security
+
+    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:
+        - "!^__init__"
+        - "!^__str__"
diff --git a/examples/tests.yaml b/examples/tests.yaml
index 273d20a5..4f6147d2 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -640,7 +640,7 @@ anta.tests.security:
   - VerifySSHStatus:
       # Verifies if the SSHD agent is disabled in the default VRF.
   - VerifySpecificIPSecConn:
-      # Verifies IPv4 security connections for a peer.
+      # Verifies the IPv4 security connections.
       ip_security_connections:
         - peer: 10.255.0.1
         - peer: 10.255.0.2
diff --git a/tests/units/anta_tests/test_security.py b/tests/units/anta_tests/test_security.py
index 0d4a478b..472eb7e1 100644
--- a/tests/units/anta_tests/test_security.py
+++ b/tests/units/anta_tests/test_security.py
@@ -1079,7 +1079,7 @@ DATA: list[dict[str, Any]] = [
                 },
             ]
         },
-        "expected": {"result": "failure", "messages": ["No IPv4 security connection configured for peer `10.255.0.1`."]},
+        "expected": {"result": "failure", "messages": ["Peer: 10.255.0.1 VRF: default - Not configured"]},
     },
     {
         "name": "failure-not-established",
@@ -1127,14 +1127,10 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "Expected state of IPv4 security connection `source:172.18.3.2 destination:172.18.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
-                "but found `Idle` instead.",
-                "Expected state of IPv4 security connection `source:100.64.2.2 destination:100.64.1.2 vrf:default` for peer `10.255.0.1` is `Established` "
-                "but found `Idle` instead.",
-                "Expected state of IPv4 security connection `source:100.64.2.2 destination:100.64.1.2 vrf:MGMT` for peer `10.255.0.2` is `Established` "
-                "but found `Idle` instead.",
-                "Expected state of IPv4 security connection `source:172.18.2.2 destination:172.18.1.2 vrf:MGMT` for peer `10.255.0.2` is `Established` "
-                "but found `Idle` instead.",
+                "Peer: 10.255.0.1 VRF: default Source: 172.18.3.2 Destination: 172.18.2.2 - Connection down - Expected: Established, Actual: Idle",
+                "Peer: 10.255.0.1 VRF: default Source: 100.64.2.2 Destination: 100.64.1.2 - Connection down - Expected: Established, Actual: Idle",
+                "Peer: 10.255.0.2 VRF: MGMT Source: 100.64.2.2 Destination: 100.64.1.2 - Connection down - Expected: Established, Actual: Idle",
+                "Peer: 10.255.0.2 VRF: MGMT Source: 172.18.2.2 Destination: 172.18.1.2 - Connection down - Expected: Established, Actual: Idle",
             ],
         },
     },
@@ -1194,12 +1190,10 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "Expected state of IPv4 security connection `source:172.18.3.2 destination:172.18.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
-                "but found `Idle` instead.",
-                "Expected state of IPv4 security connection `source:100.64.3.2 destination:100.64.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
-                "but found `Idle` instead.",
-                "IPv4 security connection `source:100.64.4.2 destination:100.64.1.2 vrf:default` for peer `10.255.0.2` is not found.",
-                "IPv4 security connection `source:172.18.4.2 destination:172.18.1.2 vrf:default` for peer `10.255.0.2` is not found.",
+                "Peer: 10.255.0.1 VRF: default Source: 172.18.3.2 Destination: 172.18.2.2 - Connection down - Expected: Established, Actual: Idle",
+                "Peer: 10.255.0.1 VRF: default Source: 100.64.3.2 Destination: 100.64.2.2 - Connection down - Expected: Established, Actual: Idle",
+                "Peer: 10.255.0.2 VRF: default Source: 100.64.4.2 Destination: 100.64.1.2 - Connection not found.",
+                "Peer: 10.255.0.2 VRF: default Source: 172.18.4.2 Destination: 172.18.1.2 - Connection not found.",
             ],
         },
     },