Repository: aristanetworks/anta
Pull request #957 (2024-12-09 13:22:54)

fix(anta.tests): Cleaning up security tests module (VerifyAPISSLCertificate, VerifyIPv4ACL)
# Description

Refactoring security tests module to address the following issues:

- https://github.com/aristanetworks/anta/issues/809
- https://github.com/aristanetworks/anta/issues/587
- https://github.com/aristanetworks/anta/issues/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`)


codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/aristanetworks/anta/branches/vitthalmagadum%3Afeat%2Fsecurity_ssl)

### Merging #957 will **not alter performance**

<sub>Comparing <code>vitthalmagadum:feat/security_ssl</code> (94c303b) with <code>main</code> (0c32b83)</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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [96.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [96.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


sonarqubecloud[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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


sonarqubecloud[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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


sonarqubecloud[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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


sonarqubecloud[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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)


sonarqubecloud[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=957) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=957&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=957&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=957&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=957&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=957&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=957)



Patch:
diff --git a/anta/input_models/security.py b/anta/input_models/security.py
index 5a517c57..79bdc17d 100644
--- a/anta/input_models/security.py
+++ b/anta/input_models/security.py
@@ -6,10 +6,20 @@
 from __future__ import annotations
 
 from ipaddress import IPv4Address
-from typing import Any
+from typing import TYPE_CHECKING, Any, ClassVar, get_args
 from warnings import warn
 
-from pydantic import BaseModel, ConfigDict
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+from anta.custom_types import EcdsaKeySize, EncryptionAlgorithm, RsaKeySize
+
+if TYPE_CHECKING:
+    import sys
+
+    if sys.version_info >= (3, 11):
+        from typing import Self
+    else:
+        from typing_extensions import Self
 
 
 class IPSecPeer(BaseModel):
@@ -43,6 +53,107 @@ class IPSecConn(BaseModel):
     """The IPv4 address of the destination in the security connection."""
 
 
+class APISSLCertificate(BaseModel):
+    """Model for an API SSL certificate."""
+
+    model_config = ConfigDict(extra="forbid")
+    certificate_name: str
+    """The name of the certificate to be verified."""
+    expiry_threshold: int
+    """The expiry threshold of the certificate in days."""
+    common_name: str
+    """The Common Name of the certificate."""
+    encryption_algorithm: EncryptionAlgorithm
+    """The encryption algorithm used by the certificate."""
+    key_size: RsaKeySize | EcdsaKeySize
+    """The key size (in bits) of the encryption algorithm."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the APISSLCertificate for reporting.
+
+        Examples
+        --------
+        - Certificate: SIGNING_CA.crt
+        """
+        return f"Certificate: {self.certificate_name}"
+
+    @model_validator(mode="after")
+    def validate_inputs(self) -> Self:
+        """Validate the key size provided to the APISSLCertificates class.
+
+        If encryption_algorithm is RSA then key_size should be in {2048, 3072, 4096}.
+
+        If encryption_algorithm is ECDSA then key_size should be in {256, 384, 521}.
+        """
+        if self.encryption_algorithm == "RSA" and self.key_size not in get_args(RsaKeySize):
+            msg = f"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}."
+            raise ValueError(msg)
+
+        if self.encryption_algorithm == "ECDSA" and self.key_size not in get_args(EcdsaKeySize):
+            msg = f"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}."
+            raise ValueError(msg)
+
+        return self
+
+
+class ACLEntry(BaseModel):
+    """Model for an Access Control List (ACL) entry."""
+
+    model_config = ConfigDict(extra="forbid")
+    sequence: int = Field(ge=1, le=4294967295)
+    """Sequence number of the ACL entry, used to define the order of processing. Must be between 1 and 4294967295."""
+    action: str
+    """Action of the ACL entry. Example: `deny ip any any`."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the ACLEntry for reporting.
+
+        Examples
+        --------
+        - Sequence: 10
+        """
+        return f"Sequence: {self.sequence}"
+
+
+class ACL(BaseModel):
+    """Model for an Access Control List (ACL)."""
+
+    model_config = ConfigDict(extra="forbid")
+    name: str
+    """Name of the ACL."""
+    entries: list[ACLEntry]
+    """List of the ACL entries."""
+    IPv4ACLEntry: ClassVar[type[ACLEntry]] = ACLEntry
+    """To maintain backward compatibility."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the ACL for reporting.
+
+        Examples
+        --------
+        - ACL name: Test
+        """
+        return f"ACL name: {self.name}"
+
+
+class IPv4ACL(ACL):  # pragma: no cover
+    """Alias for the ACL model to maintain backward compatibility.
+
+    When initialized, it will emit a deprecation warning and call the ACL model.
+
+    TODO: Remove this class in ANTA v2.0.0.
+    """
+
+    def __init__(self, **data: Any) -> None:  # noqa: ANN401
+        """Initialize the IPv4ACL class, emitting a deprecation warning."""
+        warn(
+            message="IPv4ACL model is deprecated and will be removed in ANTA v2.0.0. Use the ACL model instead.",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
+        super().__init__(**data)
+
+
 class IPSecPeers(IPSecPeer):  # pragma: no cover
     """Alias for the IPSecPeers model to maintain backward compatibility.
 
@@ -52,7 +163,7 @@ class IPSecPeers(IPSecPeer):  # pragma: no cover
     """
 
     def __init__(self, **data: Any) -> None:  # noqa: ANN401
-        """Initialize the IPSecPeer class, emitting a deprecation warning."""
+        """Initialize the IPSecPeers 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,


Test patch:
diff --git a/anta/tests/security.py b/anta/tests/security.py
index de78ea18..78908050 100644
--- a/anta/tests/security.py
+++ b/anta/tests/security.py
@@ -8,22 +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 typing import TYPE_CHECKING, ClassVar, get_args
+from typing import ClassVar
 
-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.custom_types import PositiveInteger
+from anta.input_models.security import ACL, APISSLCertificate, IPSecPeer, IPSecPeers
 from anta.models import AntaCommand, AntaTemplate, AntaTest
-from anta.tools import get_failed_logs, get_item, get_value
-
-if TYPE_CHECKING:
-    import sys
-
-    if sys.version_info >= (3, 11):
-        from typing import Self
-    else:
-        from typing_extensions import Self
+from anta.tools import get_item, get_value
 
 
 class VerifySSHStatus(AntaTest):
@@ -354,14 +344,27 @@ class VerifyAPIIPv6Acl(AntaTest):
 
 
 class VerifyAPISSLCertificate(AntaTest):
-    """Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.
+    """Verifies the eAPI SSL certificate.
+
+    This test performs the following checks for each certificate:
+
+      1. Validates that the certificate is not expired and meets the configured expiry threshold.
+      2. Validates that the certificate Common Name matches the expected one.
+      3. Ensures the certificate uses the specified encryption algorithm.
+      4. Verifies the certificate key matches the expected key size.
 
     Expected Results
     ----------------
-    * Success: The test will pass if the certificate's expiry date is greater than the threshold,
-                   and the certificate has the correct name, encryption algorithm, and key size.
-    * Failure: The test will fail if the certificate is expired or is going to expire,
-                   or if the certificate has an incorrect name, encryption algorithm, or key size.
+    * Success: If all of the following occur:
+        - The certificate's expiry date exceeds the configured threshold.
+        - The certificate's Common Name matches the input configuration.
+        - The encryption algorithm used by the certificate is as expected.
+        - The key size of the certificate matches the input configuration.
+    * Failure: If any of the following occur:
+        - The certificate is expired or set to expire within the defined threshold.
+        - The certificate's common name does not match the expected input.
+        - The encryption algorithm is incorrect.
+        - The key size does not match the expected input.
 
     Examples
     --------
@@ -393,38 +396,7 @@ class VerifyAPISSLCertificate(AntaTest):
 
         certificates: list[APISSLCertificate]
         """List of API SSL certificates."""
-
-        class APISSLCertificate(BaseModel):
-            """Model for an API SSL certificate."""
-
-            certificate_name: str
-            """The name of the certificate to be verified."""
-            expiry_threshold: int
-            """The expiry threshold of the certificate in days."""
-            common_name: str
-            """The common subject name of the certificate."""
-            encryption_algorithm: EncryptionAlgorithm
-            """The encryption algorithm of the certificate."""
-            key_size: RsaKeySize | EcdsaKeySize
-            """The encryption algorithm key size of the certificate."""
-
-            @model_validator(mode="after")
-            def validate_inputs(self) -> Self:
-                """Validate the key size provided to the APISSLCertificates class.
-
-                If encryption_algorithm is RSA then key_size should be in {2048, 3072, 4096}.
-
-                If encryption_algorithm is ECDSA then key_size should be in {256, 384, 521}.
-                """
-                if self.encryption_algorithm == "RSA" and self.key_size not in get_args(RsaKeySize):
-                    msg = f"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}."
-                    raise ValueError(msg)
-
-                if self.encryption_algorithm == "ECDSA" and self.key_size not in get_args(EcdsaKeySize):
-                    msg = f"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}."
-                    raise ValueError(msg)
-
-                return self
+        APISSLCertificate: ClassVar[type[APISSLCertificate]] = APISSLCertificate
 
     @AntaTest.anta_test
     def test(self) -> None:
@@ -442,7 +414,7 @@ class VerifyAPISSLCertificate(AntaTest):
             # Collecting certificate expiry time and current EOS time.
             # These times are used to calculate the number of days until the certificate expires.
             if not (certificate_data := get_value(certificate_output, f"certificates..{certificate.certificate_name}", separator="..")):
-                self.result.is_failure(f"SSL certificate '{certificate.certificate_name}', is not configured.\n")
+                self.result.is_failure(f"{certificate} - Not found")
                 continue
 
             expiry_time = certificate_data["notAfter"]
@@ -450,24 +422,25 @@ class VerifyAPISSLCertificate(AntaTest):
 
             # Verify certificate expiry
             if 0 < day_difference < certificate.expiry_threshold:
-                self.result.is_failure(f"SSL certificate `{certificate.certificate_name}` is about to expire in {day_difference} days.\n")
+                self.result.is_failure(
+                    f"{certificate} - set to expire within the threshold - Threshold: {certificate.expiry_threshold} days Actual: {day_difference} days"
+                )
             elif day_difference < 0:
-                self.result.is_failure(f"SSL certificate `{certificate.certificate_name}` is expired.\n")
+                self.result.is_failure(f"{certificate} - certificate expired")
 
             # Verify certificate common subject name, encryption algorithm and key size
-            keys_to_verify = ["subject.commonName", "publicKey.encryptionAlgorithm", "publicKey.size"]
-            actual_certificate_details = {key: get_value(certificate_data, key) for key in keys_to_verify}
+            common_name = get_value(certificate_data, "subject.commonName", default="Not found")
+            encryp_algo = get_value(certificate_data, "publicKey.encryptionAlgorithm", default="Not found")
+            key_size = get_value(certificate_data, "publicKey.size", default="Not found")
 
-            expected_certificate_details = {
-                "subject.commonName": certificate.common_name,
-                "publicKey.encryptionAlgorithm": certificate.encryption_algorithm,
-                "publicKey.size": certificate.key_size,
-            }
+            if common_name != certificate.common_name:
+                self.result.is_failure(f"{certificate} - incorrect common name - Expected: {certificate.common_name} Actual: {common_name}")
+
+            if encryp_algo != certificate.encryption_algorithm:
+                self.result.is_failure(f"{certificate} - incorrect encryption algorithm - Expected: {certificate.encryption_algorithm} Actual: {encryp_algo}")
 
-            if actual_certificate_details != expected_certificate_details:
-                failed_log = f"SSL certificate `{certificate.certificate_name}` is not configured properly:"
-                failed_log += get_failed_logs(expected_certificate_details, actual_certificate_details)
-                self.result.is_failure(f"{failed_log}\n")
+            if key_size != certificate.key_size:
+                self.result.is_failure(f"{certificate} - incorrect public key - Expected: {certificate.key_size} Actual: {key_size}")
 
 
 class VerifyBannerLogin(AntaTest):
@@ -555,12 +528,22 @@ class VerifyBannerMotd(AntaTest):
 
 
 class VerifyIPv4ACL(AntaTest):
-    """Verifies the configuration of IPv4 ACLs.
+    """Verifies the IPv4 ACLs.
+
+    This test performs the following checks for each IPv4 ACL:
+
+      1. Validates that the IPv4 ACL is properly configured.
+      2. Validates that the sequence entries in the ACL are correctly ordered.
 
     Expected Results
     ----------------
-    * Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.
-    * Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.
+    * Success: If all of the following occur:
+        - Any IPv4 ACL entry is not configured.
+        - The sequency entries are correctly configured.
+    * Failure: If any of the following occur:
+        - The IPv4 ACL is not configured.
+        - The any IPv4 ACL entry is not configured.
+        - The action for any entry does not match the expected input.
 
     Examples
     --------
@@ -586,65 +569,37 @@ class VerifyIPv4ACL(AntaTest):
     """
 
     categories: ClassVar[list[str]] = ["security"]
-    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip access-lists {acl}", revision=1)]
+    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip access-lists", revision=1)]
 
     class Input(AntaTest.Input):
         """Input model for the VerifyIPv4ACL test."""
 
-        ipv4_access_lists: list[IPv4ACL]
+        ipv4_access_lists: list[ACL]
         """List of IPv4 ACLs to verify."""
-
-        class IPv4ACL(BaseModel):
-            """Model for an IPv4 ACL."""
-
-            name: str
-            """Name of IPv4 ACL."""
-
-            entries: list[IPv4ACLEntry]
-            """List of IPv4 ACL entries."""
-
-            class IPv4ACLEntry(BaseModel):
-                """Model for an IPv4 ACL entry."""
-
-                sequence: int = Field(ge=1, le=4294967295)
-                """Sequence number of an ACL entry."""
-                action: str
-                """Action of an ACL entry."""
-
-    def render(self, template: AntaTemplate) -> list[AntaCommand]:
-        """Render the template for each input ACL."""
-        return [template.render(acl=acl.name) for acl in self.inputs.ipv4_access_lists]
+        IPv4ACL: ClassVar[type[ACL]] = ACL
+        """To maintain backward compatibility."""
 
     @AntaTest.anta_test
     def test(self) -> None:
         """Main test function for VerifyIPv4ACL."""
         self.result.is_success()
-        for command_output, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):
-            # Collecting input ACL details
-            acl_name = command_output.params.acl
-            # Retrieve the expected entries from the inputs
-            acl_entries = acl.entries
-
-            # Check if ACL is configured
-            ipv4_acl_list = command_output.json_output["aclList"]
-            if not ipv4_acl_list:
-                self.result.is_failure(f"{acl_name}: Not found")
+
+        if not (command_output := self.instance_commands[0].json_output["aclList"]):
+            self.result.is_failure("No Access Control List (ACL) configured")
+            return
+
+        for access_list in self.inputs.ipv4_access_lists:
+            if not (access_list_output := get_item(command_output, "name", access_list.name)):
+                self.result.is_failure(f"{access_list} - Not configured")
                 continue
 
-            # Check if the sequence number is configured and has the correct action applied
-            failed_log = f"{acl_name}:\n"
-            for acl_entry in acl_entries:
-                acl_seq = acl_entry.sequence
-                acl_action = acl_entry.action
-                if (actual_entry := get_item(ipv4_acl_list[0]["sequence"], "sequenceNumber", acl_seq)) is None:
-                    failed_log += f"Sequence number `{acl_seq}` is not found.\n"
+            for entry in access_list.entries:
+                if not (actual_entry := get_item(access_list_output["sequence"], "sequenceNumber", entry.sequence)):
+                    self.result.is_failure(f"{access_list} {entry} - Not configured")
                     continue
 
-                if actual_entry["text"] != acl_action:
-                    failed_log += f"Expected `{acl_action}` as sequence number {acl_seq} action but found `{actual_entry['text']}` instead.\n"
-
-            if failed_log != f"{acl_name}:\n":
-                self.result.is_failure(f"{failed_log}")
+                if (act_action := actual_entry["text"]) != entry.action:
+                    self.result.is_failure(f"{access_list} {entry} - action mismatch - Expected: {entry.action} Actual: {act_action}")
 
 
 class VerifyIPSecConnHealth(AntaTest):
diff --git a/examples/tests.yaml b/examples/tests.yaml
index 9aaf15e6..6a3ff456 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -663,7 +663,7 @@ anta.tests.security:
       number: 3
       vrf: default
   - VerifyAPISSLCertificate:
-      # Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.
+      # Verifies the eAPI SSL certificate.
       certificates:
         - certificate_name: ARISTA_SIGNING_CA.crt
           expiry_threshold: 30
@@ -692,7 +692,7 @@ anta.tests.security:
   - VerifyIPSecConnHealth:
       # Verifies all IPv4 security connections.
   - VerifyIPv4ACL:
-      # Verifies the configuration of IPv4 ACLs.
+      # Verifies the IPv4 ACLs.
       ipv4_access_lists:
         - name: default-control-plane-acl
           entries:
diff --git a/tests/units/anta_tests/test_security.py b/tests/units/anta_tests/test_security.py
index 4d51c96f..a3e71013 100644
--- a/tests/units/anta_tests/test_security.py
+++ b/tests/units/anta_tests/test_security.py
@@ -341,7 +341,7 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {
             "result": "failure",
-            "messages": ["SSL certificate 'ARISTA_ROOT_CA.crt', is not configured.\n"],
+            "messages": ["Certificate: ARISTA_ROOT_CA.crt - Not found"],
         },
     },
     {
@@ -366,13 +366,6 @@ DATA: list[dict[str, Any]] = [
         ],
         "inputs": {
             "certificates": [
-                {
-                    "certificate_name": "ARISTA_SIGNING_CA.crt",
-                    "expiry_threshold": 30,
-                    "common_name": "AristaIT-ICA ECDSA Issuing Cert Authority",
-                    "encryption_algorithm": "ECDSA",
-                    "key_size": 256,
-                },
                 {
                     "certificate_name": "ARISTA_ROOT_CA.crt",
                     "expiry_threshold": 30,
@@ -384,7 +377,7 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {
             "result": "failure",
-            "messages": ["SSL certificate 'ARISTA_SIGNING_CA.crt', is not configured.\n", "SSL certificate `ARISTA_ROOT_CA.crt` is expired.\n"],
+            "messages": ["Certificate: ARISTA_ROOT_CA.crt - certificate expired"],
         },
     },
     {
@@ -403,7 +396,7 @@ DATA: list[dict[str, Any]] = [
                     },
                     "ARISTA_SIGNING_CA.crt": {
                         "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"},
-                        "notAfter": 1702533518,
+                        "notAfter": 1705992709,
                         "publicKey": {
                             "encryptionAlgorithm": "ECDSA",
                             "size": 256,
@@ -435,7 +428,9 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {
             "result": "failure",
-            "messages": ["SSL certificate `ARISTA_SIGNING_CA.crt` is expired.\n", "SSL certificate `ARISTA_ROOT_CA.crt` is about to expire in 25 days."],
+            "messages": [
+                "Certificate: ARISTA_ROOT_CA.crt - set to expire within the threshold - Threshold: 30 days Actual: 25 days",
+            ],
         },
     },
     {
@@ -487,12 +482,10 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "SSL certificate `ARISTA_SIGNING_CA.crt` is not configured properly:\n"
-                "Expected `AristaIT-ICA ECDSA Issuing Cert Authority` as the subject.commonName, but found "
-                "`Arista ECDSA Issuing Cert Authority` instead.\n",
-                "SSL certificate `ARISTA_ROOT_CA.crt` is not configured properly:\n"
-                "Expected `Arista Networks Internal IT Root Cert Authority` as the subject.commonName, "
-                "but found `AristaIT-ICA Networks Internal IT Root Cert Authority` instead.\n",
+                "Certificate: ARISTA_SIGNING_CA.crt - incorrect common name - Expected: AristaIT-ICA ECDSA Issuing Cert Authority "
+                "Actual: Arista ECDSA Issuing Cert Authority",
+                "Certificate: ARISTA_ROOT_CA.crt - incorrect common name - Expected: Arista Networks Internal IT Root Cert Authority "
+                "Actual: AristaIT-ICA Networks Internal IT Root Cert Authority",
             ],
         },
     },
@@ -545,17 +538,15 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "SSL certificate `ARISTA_SIGNING_CA.crt` is not configured properly:\n"
-                "Expected `ECDSA` as the publicKey.encryptionAlgorithm, but found `RSA` instead.\n"
-                "Expected `256` as the publicKey.size, but found `4096` instead.\n",
-                "SSL certificate `ARISTA_ROOT_CA.crt` is not configured properly:\n"
-                "Expected `RSA` as the publicKey.encryptionAlgorithm, but found `ECDSA` instead.\n"
-                "Expected `4096` as the publicKey.size, but found `256` instead.\n",
+                "Certificate: ARISTA_SIGNING_CA.crt - incorrect encryption algorithm - Expected: ECDSA Actual: RSA",
+                "Certificate: ARISTA_SIGNING_CA.crt - incorrect public key - Expected: 256 Actual: 4096",
+                "Certificate: ARISTA_ROOT_CA.crt - incorrect encryption algorithm - Expected: RSA Actual: ECDSA",
+                "Certificate: ARISTA_ROOT_CA.crt - incorrect public key - Expected: 4096 Actual: 256",
             ],
         },
     },
     {
-        "name": "failure-missing-actual-output",
+        "name": "failure-missing-algorithm-details",
         "test": VerifyAPISSLCertificate,
         "eos_data": [
             {
@@ -595,12 +586,10 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "SSL certificate `ARISTA_SIGNING_CA.crt` is not configured properly:\n"
-                "Expected `ECDSA` as the publicKey.encryptionAlgorithm, but it was not found in the actual output.\n"
-                "Expected `256` as the publicKey.size, but it was not found in the actual output.\n",
-                "SSL certificate `ARISTA_ROOT_CA.crt` is not configured properly:\n"
-                "Expected `RSA` as the publicKey.encryptionAlgorithm, but it was not found in the actual output.\n"
-                "Expected `4096` as the publicKey.size, but it was not found in the actual output.\n",
+                "Certificate: ARISTA_SIGNING_CA.crt - incorrect encryption algorithm - Expected: ECDSA Actual: Not found",
+                "Certificate: ARISTA_SIGNING_CA.crt - incorrect public key - Expected: 256 Actual: Not found",
+                "Certificate: ARISTA_ROOT_CA.crt - incorrect encryption algorithm - Expected: RSA Actual: Not found",
+                "Certificate: ARISTA_ROOT_CA.crt - incorrect public key - Expected: 4096 Actual: Not found",
             ],
         },
     },
@@ -717,22 +706,20 @@ DATA: list[dict[str, Any]] = [
             {
                 "aclList": [
                     {
+                        "name": "default-control-plane-acl",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit ip any any tracked", "sequenceNumber": 20},
                             {"text": "permit udp any any eq bfd ttl eq 255", "sequenceNumber": 30},
                         ],
-                    }
-                ]
-            },
-            {
-                "aclList": [
+                    },
                     {
+                        "name": "LabTest",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 20},
                         ],
-                    }
+                    },
                 ]
             },
         ],
@@ -754,6 +741,24 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {"result": "success"},
     },
+    {
+        "name": "failure-no-acl-list",
+        "test": VerifyIPv4ACL,
+        "eos_data": [
+            {"aclList": []},
+        ],
+        "inputs": {
+            "ipv4_access_lists": [
+                {
+                    "name": "default-control-plane-acl",
+                    "entries": [
+                        {"sequence": 10, "action": "permit icmp any any"},
+                    ],
+                },
+            ]
+        },
+        "expected": {"result": "failure", "messages": ["No Access Control List (ACL) configured"]},
+    },
     {
         "name": "failure-acl-not-found",
         "test": VerifyIPv4ACL,
@@ -761,6 +766,7 @@ DATA: list[dict[str, Any]] = [
             {
                 "aclList": [
                     {
+                        "name": "default-control-plane-acl",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit ip any any tracked", "sequenceNumber": 20},
@@ -769,7 +775,6 @@ DATA: list[dict[str, Any]] = [
                     }
                 ]
             },
-            {"aclList": []},
         ],
         "inputs": {
             "ipv4_access_lists": [
@@ -787,7 +792,7 @@ DATA: list[dict[str, Any]] = [
                 },
             ]
         },
-        "expected": {"result": "failure", "messages": ["LabTest: Not found"]},
+        "expected": {"result": "failure", "messages": ["ACL name: LabTest - Not configured"]},
     },
     {
         "name": "failure-sequence-not-found",
@@ -796,22 +801,20 @@ DATA: list[dict[str, Any]] = [
             {
                 "aclList": [
                     {
+                        "name": "default-control-plane-acl",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit ip any any tracked", "sequenceNumber": 20},
                             {"text": "permit udp any any eq bfd ttl eq 255", "sequenceNumber": 40},
                         ],
-                    }
-                ]
-            },
-            {
-                "aclList": [
+                    },
                     {
+                        "name": "LabTest",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 30},
                         ],
-                    }
+                    },
                 ]
             },
         ],
@@ -833,7 +836,7 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {
             "result": "failure",
-            "messages": ["default-control-plane-acl:\nSequence number `30` is not found.\n", "LabTest:\nSequence number `20` is not found.\n"],
+            "messages": ["ACL name: default-control-plane-acl Sequence: 30 - Not configured", "ACL name: LabTest Sequence: 20 - Not configured"],
         },
     },
     {
@@ -843,22 +846,20 @@ DATA: list[dict[str, Any]] = [
             {
                 "aclList": [
                     {
+                        "name": "default-control-plane-acl",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit ip any any tracked", "sequenceNumber": 20},
                             {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 30},
                         ],
-                    }
-                ]
-            },
-            {
-                "aclList": [
+                    },
                     {
+                        "name": "LabTest",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit udp any any eq bfd ttl eq 255", "sequenceNumber": 20},
                         ],
-                    }
+                    },
                 ]
             },
         ],
@@ -881,9 +882,9 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "default-control-plane-acl:\n"
-                "Expected `permit udp any any eq bfd ttl eq 255` as sequence number 30 action but found `permit tcp any any range 5900 5910` instead.\n",
-                "LabTest:\nExpected `permit tcp any any range 5900 5910` as sequence number 20 action but found `permit udp any any eq bfd ttl eq 255` instead.\n",
+                "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - Expected: permit udp any any eq bfd ttl eq 255 "
+                "Actual: permit tcp any any range 5900 5910",
+                "ACL name: LabTest Sequence: 20 - action mismatch - Expected: permit tcp any any range 5900 5910 Actual: permit udp any any eq bfd ttl eq 255",
             ],
         },
     },
@@ -894,6 +895,7 @@ DATA: list[dict[str, Any]] = [
             {
                 "aclList": [
                     {
+                        "name": "default-control-plane-acl",
                         "sequence": [
                             {"text": "permit icmp any any", "sequenceNumber": 10},
                             {"text": "permit ip any any tracked", "sequenceNumber": 40},
@@ -902,7 +904,6 @@ DATA: list[dict[str, Any]] = [
                     }
                 ]
             },
-            {"aclList": []},
         ],
         "inputs": {
             "ipv4_access_lists": [
@@ -923,9 +924,10 @@ DATA: list[dict[str, Any]] = [
         "expected": {
             "result": "failure",
             "messages": [
-                "default-control-plane-acl:\nSequence number `20` is not found.\n"
-                "Expected `permit udp any any eq bfd ttl eq 255` as sequence number 30 action but found `permit tcp any any range 5900 5910` instead.\n",
-                "LabTest: Not found",
+                "ACL name: default-control-plane-acl Sequence: 20 - Not configured",
+                "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - Expected: permit udp any any eq bfd ttl eq 255 "
+                "Actual: permit tcp any any range 5900 5910",
+                "ACL name: LabTest - Not configured",
             ],
         },
     },