8
8
# Mypy does not understand AntaTest.Input typing
9
9
# mypy: disable-error-code=attr-defined
10
10
from datetime import datetime , timezone
11
- from typing import TYPE_CHECKING , ClassVar , get_args
11
+ from typing import ClassVar
12
12
13
- from pydantic import BaseModel , Field , model_validator
14
-
15
- from anta .custom_types import EcdsaKeySize , EncryptionAlgorithm , PositiveInteger , RsaKeySize
16
- from anta .input_models .security import IPSecPeer , IPSecPeers
13
+ from anta .custom_types import PositiveInteger
14
+ from anta .input_models .security import ACL , APISSLCertificate , IPSecPeer , IPSecPeers
17
15
from anta .models import AntaCommand , AntaTemplate , AntaTest
18
- from anta .tools import get_failed_logs , get_item , get_value
19
-
20
- if TYPE_CHECKING :
21
- import sys
22
-
23
- if sys .version_info >= (3 , 11 ):
24
- from typing import Self
25
- else :
26
- from typing_extensions import Self
16
+ from anta .tools import get_item , get_value
27
17
28
18
29
19
class VerifySSHStatus (AntaTest ):
@@ -354,14 +344,27 @@ def test(self) -> None:
354
344
355
345
356
346
class VerifyAPISSLCertificate (AntaTest ):
357
- """Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.
347
+ """Verifies the eAPI SSL certificate.
348
+
349
+ This test performs the following checks for each certificate:
350
+
351
+ 1. Validates that the certificate is not expired and meets the configured expiry threshold.
352
+ 2. Validates that the certificate Common Name matches the expected one.
353
+ 3. Ensures the certificate uses the specified encryption algorithm.
354
+ 4. Verifies the certificate key matches the expected key size.
358
355
359
356
Expected Results
360
357
----------------
361
- * Success: The test will pass if the certificate's expiry date is greater than the threshold,
362
- and the certificate has the correct name, encryption algorithm, and key size.
363
- * Failure: The test will fail if the certificate is expired or is going to expire,
364
- or if the certificate has an incorrect name, encryption algorithm, or key size.
358
+ * Success: If all of the following occur:
359
+ - The certificate's expiry date exceeds the configured threshold.
360
+ - The certificate's Common Name matches the input configuration.
361
+ - The encryption algorithm used by the certificate is as expected.
362
+ - The key size of the certificate matches the input configuration.
363
+ * Failure: If any of the following occur:
364
+ - The certificate is expired or set to expire within the defined threshold.
365
+ - The certificate's common name does not match the expected input.
366
+ - The encryption algorithm is incorrect.
367
+ - The key size does not match the expected input.
365
368
366
369
Examples
367
370
--------
@@ -393,38 +396,7 @@ class Input(AntaTest.Input):
393
396
394
397
certificates : list [APISSLCertificate ]
395
398
"""List of API SSL certificates."""
396
-
397
- class APISSLCertificate (BaseModel ):
398
- """Model for an API SSL certificate."""
399
-
400
- certificate_name : str
401
- """The name of the certificate to be verified."""
402
- expiry_threshold : int
403
- """The expiry threshold of the certificate in days."""
404
- common_name : str
405
- """The common subject name of the certificate."""
406
- encryption_algorithm : EncryptionAlgorithm
407
- """The encryption algorithm of the certificate."""
408
- key_size : RsaKeySize | EcdsaKeySize
409
- """The encryption algorithm key size of the certificate."""
410
-
411
- @model_validator (mode = "after" )
412
- def validate_inputs (self ) -> Self :
413
- """Validate the key size provided to the APISSLCertificates class.
414
-
415
- If encryption_algorithm is RSA then key_size should be in {2048, 3072, 4096}.
416
-
417
- If encryption_algorithm is ECDSA then key_size should be in {256, 384, 521}.
418
- """
419
- if self .encryption_algorithm == "RSA" and self .key_size not in get_args (RsaKeySize ):
420
- msg = f"`{ self .certificate_name } ` key size { self .key_size } is invalid for RSA encryption. Allowed sizes are { get_args (RsaKeySize )} ."
421
- raise ValueError (msg )
422
-
423
- if self .encryption_algorithm == "ECDSA" and self .key_size not in get_args (EcdsaKeySize ):
424
- msg = f"`{ self .certificate_name } ` key size { self .key_size } is invalid for ECDSA encryption. Allowed sizes are { get_args (EcdsaKeySize )} ."
425
- raise ValueError (msg )
426
-
427
- return self
399
+ APISSLCertificate : ClassVar [type [APISSLCertificate ]] = APISSLCertificate
428
400
429
401
@AntaTest .anta_test
430
402
def test (self ) -> None :
@@ -442,32 +414,33 @@ def test(self) -> None:
442
414
# Collecting certificate expiry time and current EOS time.
443
415
# These times are used to calculate the number of days until the certificate expires.
444
416
if not (certificate_data := get_value (certificate_output , f"certificates..{ certificate .certificate_name } " , separator = ".." )):
445
- self .result .is_failure (f"SSL certificate ' { certificate . certificate_name } ', is not configured. \n " )
417
+ self .result .is_failure (f"{ certificate } - Not found " )
446
418
continue
447
419
448
420
expiry_time = certificate_data ["notAfter" ]
449
421
day_difference = (datetime .fromtimestamp (expiry_time , tz = timezone .utc ) - datetime .fromtimestamp (current_timestamp , tz = timezone .utc )).days
450
422
451
423
# Verify certificate expiry
452
424
if 0 < day_difference < certificate .expiry_threshold :
453
- self .result .is_failure (f"SSL certificate `{ certificate .certificate_name } ` is about to expire in { day_difference } days.\n " )
425
+ self .result .is_failure (
426
+ f"{ certificate } - set to expire within the threshold - Threshold: { certificate .expiry_threshold } days Actual: { day_difference } days"
427
+ )
454
428
elif day_difference < 0 :
455
- self .result .is_failure (f"SSL certificate ` { certificate . certificate_name } ` is expired. \n " )
429
+ self .result .is_failure (f"{ certificate } - certificate expired" )
456
430
457
431
# Verify certificate common subject name, encryption algorithm and key size
458
- keys_to_verify = ["subject.commonName" , "publicKey.encryptionAlgorithm" , "publicKey.size" ]
459
- actual_certificate_details = {key : get_value (certificate_data , key ) for key in keys_to_verify }
432
+ common_name = get_value (certificate_data , "subject.commonName" , default = "Not found" )
433
+ encryp_algo = get_value (certificate_data , "publicKey.encryptionAlgorithm" , default = "Not found" )
434
+ key_size = get_value (certificate_data , "publicKey.size" , default = "Not found" )
460
435
461
- expected_certificate_details = {
462
- "subject.commonName" : certificate .common_name ,
463
- "publicKey.encryptionAlgorithm" : certificate . encryption_algorithm ,
464
- "publicKey.size" : certificate .key_size ,
465
- }
436
+ if common_name != certificate . common_name :
437
+ self . result . is_failure ( f" { certificate } - incorrect common name - Expected: { certificate .common_name } Actual: { common_name } " )
438
+
439
+ if encryp_algo != certificate .encryption_algorithm :
440
+ self . result . is_failure ( f" { certificate } - incorrect encryption algorithm - Expected: { certificate . encryption_algorithm } Actual: { encryp_algo } " )
466
441
467
- if actual_certificate_details != expected_certificate_details :
468
- failed_log = f"SSL certificate `{ certificate .certificate_name } ` is not configured properly:"
469
- failed_log += get_failed_logs (expected_certificate_details , actual_certificate_details )
470
- self .result .is_failure (f"{ failed_log } \n " )
442
+ if key_size != certificate .key_size :
443
+ self .result .is_failure (f"{ certificate } - incorrect public key - Expected: { certificate .key_size } Actual: { key_size } " )
471
444
472
445
473
446
class VerifyBannerLogin (AntaTest ):
@@ -555,12 +528,22 @@ def test(self) -> None:
555
528
556
529
557
530
class VerifyIPv4ACL (AntaTest ):
558
- """Verifies the configuration of IPv4 ACLs.
531
+ """Verifies the IPv4 ACLs.
532
+
533
+ This test performs the following checks for each IPv4 ACL:
534
+
535
+ 1. Validates that the IPv4 ACL is properly configured.
536
+ 2. Validates that the sequence entries in the ACL are correctly ordered.
559
537
560
538
Expected Results
561
539
----------------
562
- * Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.
563
- * Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.
540
+ * Success: If all of the following occur:
541
+ - Any IPv4 ACL entry is not configured.
542
+ - The sequency entries are correctly configured.
543
+ * Failure: If any of the following occur:
544
+ - The IPv4 ACL is not configured.
545
+ - The any IPv4 ACL entry is not configured.
546
+ - The action for any entry does not match the expected input.
564
547
565
548
Examples
566
549
--------
@@ -586,65 +569,37 @@ class VerifyIPv4ACL(AntaTest):
586
569
"""
587
570
588
571
categories : ClassVar [list [str ]] = ["security" ]
589
- commands : ClassVar [list [AntaCommand | AntaTemplate ]] = [AntaTemplate ( template = "show ip access-lists {acl} " , revision = 1 )]
572
+ commands : ClassVar [list [AntaCommand | AntaTemplate ]] = [AntaCommand ( command = "show ip access-lists" , revision = 1 )]
590
573
591
574
class Input (AntaTest .Input ):
592
575
"""Input model for the VerifyIPv4ACL test."""
593
576
594
- ipv4_access_lists : list [IPv4ACL ]
577
+ ipv4_access_lists : list [ACL ]
595
578
"""List of IPv4 ACLs to verify."""
596
-
597
- class IPv4ACL (BaseModel ):
598
- """Model for an IPv4 ACL."""
599
-
600
- name : str
601
- """Name of IPv4 ACL."""
602
-
603
- entries : list [IPv4ACLEntry ]
604
- """List of IPv4 ACL entries."""
605
-
606
- class IPv4ACLEntry (BaseModel ):
607
- """Model for an IPv4 ACL entry."""
608
-
609
- sequence : int = Field (ge = 1 , le = 4294967295 )
610
- """Sequence number of an ACL entry."""
611
- action : str
612
- """Action of an ACL entry."""
613
-
614
- def render (self , template : AntaTemplate ) -> list [AntaCommand ]:
615
- """Render the template for each input ACL."""
616
- return [template .render (acl = acl .name ) for acl in self .inputs .ipv4_access_lists ]
579
+ IPv4ACL : ClassVar [type [ACL ]] = ACL
580
+ """To maintain backward compatibility."""
617
581
618
582
@AntaTest .anta_test
619
583
def test (self ) -> None :
620
584
"""Main test function for VerifyIPv4ACL."""
621
585
self .result .is_success ()
622
- for command_output , acl in zip (self .instance_commands , self .inputs .ipv4_access_lists ):
623
- # Collecting input ACL details
624
- acl_name = command_output .params .acl
625
- # Retrieve the expected entries from the inputs
626
- acl_entries = acl .entries
627
-
628
- # Check if ACL is configured
629
- ipv4_acl_list = command_output .json_output ["aclList" ]
630
- if not ipv4_acl_list :
631
- self .result .is_failure (f"{ acl_name } : Not found" )
586
+
587
+ if not (command_output := self .instance_commands [0 ].json_output ["aclList" ]):
588
+ self .result .is_failure ("No Access Control List (ACL) configured" )
589
+ return
590
+
591
+ for access_list in self .inputs .ipv4_access_lists :
592
+ if not (access_list_output := get_item (command_output , "name" , access_list .name )):
593
+ self .result .is_failure (f"{ access_list } - Not configured" )
632
594
continue
633
595
634
- # Check if the sequence number is configured and has the correct action applied
635
- failed_log = f"{ acl_name } :\n "
636
- for acl_entry in acl_entries :
637
- acl_seq = acl_entry .sequence
638
- acl_action = acl_entry .action
639
- if (actual_entry := get_item (ipv4_acl_list [0 ]["sequence" ], "sequenceNumber" , acl_seq )) is None :
640
- failed_log += f"Sequence number `{ acl_seq } ` is not found.\n "
596
+ for entry in access_list .entries :
597
+ if not (actual_entry := get_item (access_list_output ["sequence" ], "sequenceNumber" , entry .sequence )):
598
+ self .result .is_failure (f"{ access_list } { entry } - Not configured" )
641
599
continue
642
600
643
- if actual_entry ["text" ] != acl_action :
644
- failed_log += f"Expected `{ acl_action } ` as sequence number { acl_seq } action but found `{ actual_entry ['text' ]} ` instead.\n "
645
-
646
- if failed_log != f"{ acl_name } :\n " :
647
- self .result .is_failure (f"{ failed_log } " )
601
+ if (act_action := actual_entry ["text" ]) != entry .action :
602
+ self .result .is_failure (f"{ access_list } { entry } - action mismatch - Expected: { entry .action } Actual: { act_action } " )
648
603
649
604
650
605
class VerifyIPSecConnHealth (AntaTest ):
0 commit comments