anta 1.10.0.dev1__tar.gz → 1.10.0.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/PKG-INFO +1 -1
  2. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/csv_reporter.py +3 -3
  3. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/md_reporter.py +132 -53
  4. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/reporting.py +4 -4
  5. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/constants.py +1 -0
  6. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_117.py +26 -10
  7. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_140.py +2 -2
  8. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_142.py +2 -2
  9. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_146.py +2 -2
  10. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_147.py +5 -5
  11. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/stp.py +4 -1
  12. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/PKG-INFO +1 -1
  13. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/pyproject.toml +3 -3
  14. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/LICENSE +0 -0
  15. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/README.md +0 -0
  16. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/__init__.py +0 -0
  17. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/__init__.py +0 -0
  18. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/base.py +0 -0
  19. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/eos_versions.py +0 -0
  20. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/models.py +0 -0
  21. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/optional_commands.py +0 -0
  22. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/platforms.py +0 -0
  23. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/remediation.py +0 -0
  24. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/__init__.py +0 -0
  25. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/results.py +0 -0
  26. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/status.py +0 -0
  27. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_eos/__init__.py +0 -0
  28. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_eos/version.py +0 -0
  29. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_runner.py +0 -0
  30. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/catalog.py +0 -0
  31. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/__init__.py +0 -0
  32. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/_main.py +0 -0
  33. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/check/__init__.py +0 -0
  34. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/check/commands.py +0 -0
  35. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/console.py +0 -0
  36. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/__init__.py +0 -0
  37. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/commands.py +0 -0
  38. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/utils.py +0 -0
  39. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/__init__.py +0 -0
  40. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/commands.py +0 -0
  41. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/utils.py +0 -0
  42. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/__init__.py +0 -0
  43. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/commands.py +0 -0
  44. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/utils.py +0 -0
  45. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/__init__.py +0 -0
  46. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/commands.py +0 -0
  47. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/utils.py +0 -0
  48. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/psirt.py +0 -0
  49. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/utils.py +0 -0
  50. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/custom_types.py +0 -0
  51. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/decorators.py +0 -0
  52. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/device.py +0 -0
  53. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/__init__.py +0 -0
  54. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/avt.py +0 -0
  55. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/bfd.py +0 -0
  56. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/configuration.py +0 -0
  57. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/connectivity.py +0 -0
  58. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/cvx.py +0 -0
  59. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/evpn.py +0 -0
  60. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/flow_tracking.py +0 -0
  61. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/hardware.py +0 -0
  62. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/interfaces.py +0 -0
  63. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/logging.py +0 -0
  64. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/path_selection.py +0 -0
  65. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/__init__.py +0 -0
  66. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/bgp.py +0 -0
  67. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/generic.py +0 -0
  68. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/isis.py +0 -0
  69. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/ospf.py +0 -0
  70. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/security.py +0 -0
  71. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/services.py +0 -0
  72. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/snmp.py +0 -0
  73. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/stun.py +0 -0
  74. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/system.py +0 -0
  75. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/vlan.py +0 -0
  76. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/__init__.py +0 -0
  77. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/exceptions.py +0 -0
  78. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/models.py +0 -0
  79. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/logger.py +0 -0
  80. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/models.py +0 -0
  81. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/py.typed +0 -0
  82. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/__init__.py +0 -0
  83. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/csv_reporter.py +0 -0
  84. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/jinja_reporter.py +0 -0
  85. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/md_reporter.py +0 -0
  86. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/table_reporter.py +0 -0
  87. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/result_manager/__init__.py +0 -0
  88. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/result_manager/models.py +0 -0
  89. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/runner.py +0 -0
  90. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/settings.py +0 -0
  91. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/__init__.py +0 -0
  92. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/aaa.py +0 -0
  93. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/__init__.py +0 -0
  94. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/avt.py +0 -0
  95. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/bfd.py +0 -0
  96. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/configuration.py +0 -0
  97. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/connectivity.py +0 -0
  98. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/cvx.py +0 -0
  99. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/evpn.py +0 -0
  100. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/field_notices.py +0 -0
  101. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/flow_tracking.py +0 -0
  102. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/greent.py +0 -0
  103. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/hardware.py +0 -0
  104. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/interfaces.py +0 -0
  105. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/lanz.py +0 -0
  106. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/logging.py +0 -0
  107. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/mlag.py +0 -0
  108. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/multicast.py +0 -0
  109. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/path_selection.py +0 -0
  110. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/profiles.py +0 -0
  111. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/ptp.py +0 -0
  112. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/__init__.py +0 -0
  113. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/bgp.py +0 -0
  114. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/generic.py +0 -0
  115. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/isis.py +0 -0
  116. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/ospf.py +0 -0
  117. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/security.py +0 -0
  118. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/services.py +0 -0
  119. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/snmp.py +0 -0
  120. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/software.py +0 -0
  121. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/stun.py +0 -0
  122. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/system.py +0 -0
  123. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/vlan.py +0 -0
  124. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/vxlan.py +0 -0
  125. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tools.py +0 -0
  126. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/SOURCES.txt +0 -0
  127. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/dependency_links.txt +0 -0
  128. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/entry_points.txt +0 -0
  129. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/requires.txt +0 -0
  130. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/top_level.txt +0 -0
  131. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/__init__.py +0 -0
  132. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_auth.py +0 -0
  133. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_constants.py +0 -0
  134. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_errors.py +0 -0
  135. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_models.py +0 -0
  136. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_types.py +0 -0
  137. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/aio_portcheck.py +0 -0
  138. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/config_session.py +0 -0
  139. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/device.py +0 -0
  140. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/errors.py +0 -0
  141. {anta-1.10.0.dev1 → anta-1.10.0.dev3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anta
3
- Version: 1.10.0.dev1
3
+ Version: 1.10.0.dev3
4
4
  Summary: Arista Network Test Automation (ANTA) Framework
5
5
  Author-email: Arista Networks ANTA maintainers <anta-dev@arista.com>
6
6
  Maintainer-email: Arista Networks ANTA maintainers <anta-dev@arista.com>, Khelil Sator <ksator@arista.com>, Matthieu Tâche <mtache@arista.com>, Thomas Grimonet <tgrimonet@arista.com>, Guillaume Mulocher <gmulocher@arista.com>, Carl Baillargeon <carl.baillargeon@arista.com>
@@ -54,7 +54,7 @@ class SecurityAdvisoryReportCsv(ReportCsv):
54
54
  def _format_remediations(result: TestResult | AtomicTestResult) -> str:
55
55
  """Flatten advisory remediation entries into a plain-text CSV cell."""
56
56
  if isinstance(result, (_AdvisoryTestResult, _AdvisoryAtomicTestResult)):
57
- return "\n".join(result.remediations)
57
+ return ReportCsv.split_list_to_txt_list(result.remediations, "\\n")
58
58
  return ""
59
59
 
60
60
  @classmethod
@@ -70,9 +70,9 @@ class SecurityAdvisoryReportCsv(ReportCsv):
70
70
  str(result.name),
71
71
  result.test,
72
72
  cls._format_result(result),
73
- "\n".join(result.messages),
73
+ cls.split_list_to_txt_list(result.messages, "\\n"),
74
74
  cls._format_result(row_result),
75
- "\n".join(row_result.messages),
75
+ cls.split_list_to_txt_list(row_result.messages, "\\n"),
76
76
  cls._format_remediations(row_result),
77
77
  cls._format_remediations(result),
78
78
  f"SA{advisory.sa_number}",
@@ -7,9 +7,9 @@ from __future__ import annotations
7
7
 
8
8
  from typing import TYPE_CHECKING, ClassVar
9
9
 
10
- from anta._advisory.models import _AdvisoryMetadata, _AdvisoryVulnerabilitySeverity
10
+ from anta._advisory.models import _ADVISORY_VULNERABILITY_SEVERITY_RANK, _AdvisoryMetadata, _AdvisoryVulnerabilitySeverity
11
11
  from anta._advisory.reporter.reporting import SecurityAdvisoryRunOverviewData, _get_advisory_result
12
- from anta._advisory.results import _get_atomic_vulnerability_ids
12
+ from anta._advisory.results import _AdvisoryAtomicTestResult, _AdvisoryTestResult, _get_atomic_vulnerability_ids
13
13
  from anta.reporter.md_reporter import MDReportBase
14
14
  from anta.result_manager.models import AntaTestStatus
15
15
 
@@ -34,7 +34,7 @@ SEVERITY_ICONS = {
34
34
  RESULT_ICONS = {
35
35
  AntaTestStatus.SUCCESS: "✅",
36
36
  AntaTestStatus.INCONCLUSIVE: "❓",
37
- AntaTestStatus.FAILURE: "",
37
+ AntaTestStatus.FAILURE: "🛑",
38
38
  AntaTestStatus.ERROR: "❗",
39
39
  AntaTestStatus.SKIPPED: "⏭️",
40
40
  AntaTestStatus.UNSET: "-",
@@ -64,6 +64,65 @@ class SecurityAdvisoryMDReportBase(MDReportBase):
64
64
  """Format an ANTA result using advisory-facing terminology."""
65
65
  return f"{RESULT_ICONS[result.result]}&nbsp;{_get_advisory_result(result).title()}"
66
66
 
67
+ @staticmethod
68
+ def format_severity(severity: _AdvisoryVulnerabilitySeverity) -> str:
69
+ """Format a vulnerability severity with its identifying icon."""
70
+ return f"{SEVERITY_ICONS[severity]}&nbsp;{severity.value.title()}"
71
+
72
+ def format_remediations(self, result: TestResult | AtomicTestResult) -> str:
73
+ """Format advisory remediation entries for a Markdown table cell."""
74
+ if not isinstance(result, (_AdvisoryTestResult, _AdvisoryAtomicTestResult)):
75
+ return "-"
76
+ remediations = list(result.remediations)
77
+ atomic_remediations: list[str] = []
78
+ if isinstance(result, _AdvisoryTestResult):
79
+ atomic_remediations = [
80
+ remediation for atomic in result.atomic_results if isinstance(atomic, _AdvisoryAtomicTestResult) for remediation in atomic.remediations
81
+ ]
82
+ remediations.extend(atomic_remediations)
83
+ unique_remediations = dict.fromkeys(remediations)
84
+ formatted_remediations = []
85
+ for remediation in unique_remediations:
86
+ vulnerability_ids = self._remediation_vulnerability_ids(result, remediation)
87
+ prefix = f"{', '.join(vulnerability_ids)}: " if vulnerability_ids else ""
88
+ formatted_remediations.append(f"{prefix}{remediation}")
89
+ if atomic_remediations:
90
+ return "<br>".join(f"•&nbsp;{self.safe_markdown(remediation)}" for remediation in formatted_remediations)
91
+ return self.safe_markdown("\n".join(formatted_remediations)) or "-"
92
+
93
+ @staticmethod
94
+ def _remediation_vulnerability_ids(result: TestResult | AtomicTestResult, remediation: str) -> tuple[str, ...]:
95
+ """Return advisory-ordered vulnerability IDs sharing a parent remediation."""
96
+ if not isinstance(result, _AdvisoryTestResult):
97
+ return ()
98
+
99
+ associated_ids: set[str] = set()
100
+ for atomic in result.atomic_results:
101
+ if not isinstance(atomic, _AdvisoryAtomicTestResult) or remediation not in atomic.remediations:
102
+ continue
103
+ if not atomic.vulnerability_ids:
104
+ return ()
105
+ associated_ids.update(atomic.vulnerability_ids)
106
+ return tuple(vulnerability.id for vulnerability in result.advisory.vulnerabilities if vulnerability.id in associated_ids)
107
+
108
+ def format_findings(self, result: TestResult) -> str:
109
+ """Format findings and label associated atomic findings with vulnerability IDs."""
110
+ messages = list(result.messages)
111
+ if isinstance(result, _AdvisoryTestResult):
112
+ for atomic in result.atomic_results:
113
+ vulnerability_ids = _get_atomic_vulnerability_ids(atomic)
114
+ if not vulnerability_ids:
115
+ continue
116
+ prefix = f"{', '.join(vulnerability_ids)}: "
117
+ for message in atomic.messages:
118
+ inherited_message = f"{atomic.description} - {message}"
119
+ try:
120
+ index = messages.index(inherited_message)
121
+ except ValueError:
122
+ continue
123
+ messages[index] = f"{prefix}{message}"
124
+ return self.safe_markdown("<br>".join(messages)) or "-"
125
+
67
126
 
68
127
  class ANTASecurityAdvisoryReport(SecurityAdvisoryMDReportBase):
69
128
  """Generate the title and table of contents for a security advisory report."""
@@ -72,15 +131,18 @@ class ANTASecurityAdvisoryReport(SecurityAdvisoryMDReportBase):
72
131
 
73
132
  def generate_section(self) -> None:
74
133
  """Generate the security advisory report heading and table of contents."""
75
- self.write_heading(heading_level=1)
76
- toc = "**Table of Contents:**\n\n- [ANTA Security Advisory Report](#anta-security-advisory-report)\n"
134
+ self.mdfile.write('<h1 id="anta-security-advisory-report" align="center">🛡️ ANTA Security Advisory Report 🛡️</h1>\n\n')
135
+ toc = "**Table of Contents:**\n\n"
77
136
  if self.groups:
78
- toc += " - [Advisory Exposure Summary](#advisory-exposure-summary)\n - [Security Advisory Details](#security-advisory-details)\n"
79
- toc += " - [Security Advisory Run Overview](#security-advisory-run-overview)"
137
+ toc += "- [Advisory Assessment Summary](#advisory-assessment-summary)\n- [Security Advisory Details](#security-advisory-details)\n"
138
+ for group in self.groups:
139
+ advisory = group.advisory
140
+ toc += f" - [{self.safe_markdown(advisory.title)}](#sa-{advisory.sa_number.lower()})\n"
141
+ toc += "- [Run Overview](#run-overview)"
80
142
  self.mdfile.write(toc + "\n\n")
81
143
 
82
144
 
83
- class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
145
+ class AdvisoryAssessmentSummary(SecurityAdvisoryMDReportBase):
84
146
  """Generate a compact status summary grouped by security advisory."""
85
147
 
86
148
  ICON = "📊"
@@ -88,7 +150,7 @@ class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
88
150
  "Security Advisory",
89
151
  "Severity",
90
152
  "Devices",
91
- "❌&nbsp;Affected",
153
+ "🛑&nbsp;Affected",
92
154
  "❓&nbsp;Inconclusive",
93
155
  "✅&nbsp;Mitigated",
94
156
  "✅&nbsp;Not Affected",
@@ -106,9 +168,8 @@ class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
106
168
  """Generate one summary row per security advisory."""
107
169
  for group in self.groups:
108
170
  advisory = group.advisory
109
- advisory_link = f"[SA{advisory.sa_number}: {self.safe_markdown(advisory.title)}](#sa-{advisory.sa_number.lower()})"
110
- advisory_severity = group.severity
111
- severity = f"{SEVERITY_ICONS[advisory_severity]}&nbsp;{advisory_severity.value.title()}"
171
+ advisory_link = f"[{self.safe_markdown(advisory.title)}](#sa-{advisory.sa_number.lower()})"
172
+ severity = self.format_severity(group.severity)
112
173
  devices = len({result.name for result in group.results})
113
174
  yield (
114
175
  f"| {advisory_link} | {severity} | {devices} "
@@ -121,7 +182,7 @@ class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
121
182
  )
122
183
 
123
184
  def generate_section(self) -> None:
124
- """Generate the advisory exposure summary section."""
185
+ """Generate the advisory assessment summary section."""
125
186
  self.write_heading(heading_level=2)
126
187
  self.write_table(table_heading=self.TABLE_HEADING)
127
188
 
@@ -133,29 +194,34 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
133
194
 
134
195
  def _write_vulnerabilities(self, advisory: _AdvisoryMetadata) -> None:
135
196
  """Write vulnerability details for an advisory."""
136
- self.mdfile.write("#### Vulnerabilities\n\n")
137
- heading = self.generate_table_heading(["Vulnerability", "Description", "Severity"])
138
- self.mdfile.write("\n".join(heading) + "\n")
139
- for vulnerability in advisory.vulnerabilities:
197
+ # NOTE: Nested tables are not supported consistently by every Markdown renderer. Prefix every table line and pad it with quoted blank lines to maximize
198
+ # compatibility across renderers that support Markdown tables.
199
+ heading = self.generate_table_heading(["Vulnerability", "Severity", "Description"])
200
+ self.mdfile.write("\n".join(f"> {line}" for line in heading) + "\n")
201
+ vulnerabilities = sorted(
202
+ advisory.vulnerabilities,
203
+ key=lambda vulnerability: (-_ADVISORY_VULNERABILITY_SEVERITY_RANK[vulnerability.severity], vulnerability.id.casefold()),
204
+ )
205
+ for vulnerability in vulnerabilities:
140
206
  vulnerability_id = self.safe_markdown(vulnerability.id)
141
207
  description = self.safe_markdown(vulnerability.description)
142
- self.mdfile.write(f"| {vulnerability_id} | {description} | {vulnerability.severity.value.title()} |\n")
143
- self.mdfile.write("\n")
208
+ self.mdfile.write(f"> | {vulnerability_id} | {self.format_severity(vulnerability.severity)} | {description} |\n")
209
+ self.mdfile.write(">\n\n")
144
210
 
145
211
  def _write_findings(self, group: AdvisoryResultGroup) -> None:
146
212
  """Write per-device findings for an advisory."""
147
213
  # TODO: When revisiting Markdown reports, fall back to atomic descriptions and messages if the parent result has no messages.
148
- # TODO: Render parent and atomic remediation lists once the Markdown remediation presentation is defined.
149
214
  self.mdfile.write("#### 🔎 Device Findings\n\n")
150
215
  if self.config.expand_results:
151
216
  self._write_expanded_findings(group)
152
217
  return
153
218
 
154
- heading = self.generate_table_heading(["Device", "Test", "Result", "Messages"])
219
+ heading = self.generate_table_heading(["Device", "Result", "Findings", "Remediations"])
155
220
  self.mdfile.write("\n".join(heading) + "\n")
156
221
  for result in group.results:
157
- messages = self.safe_markdown("<br>".join(result.messages)) or "-"
158
- self.mdfile.write(f"| {self.safe_markdown(result.name)} | {self.safe_markdown(result.test)} | {self.format_advisory_result(result)} | {messages} |\n")
222
+ findings = self.format_findings(result)
223
+ remediation = self.format_remediations(result)
224
+ self.mdfile.write(f"| {self.safe_markdown(result.name)} | {self.format_advisory_result(result)} | {findings} | {remediation} |\n")
159
225
 
160
226
  @staticmethod
161
227
  def _atomic_summary(result: TestResult) -> str:
@@ -175,29 +241,33 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
175
241
 
176
242
  def _write_expanded_findings(self, group: AdvisoryResultGroup) -> None:
177
243
  """Write parent advisory results followed by their actual detailed issue results."""
178
- heading = self.generate_table_heading(["Device", "Test", "Description", "Vulnerability ID(s)", "Result", "Messages"])
244
+ heading = self.generate_table_heading(["Device", "Vulnerability ID(s)", "Result", "Findings", "Remediations"])
179
245
  self.mdfile.write("\n".join(heading) + "\n")
246
+ vulnerability_by_id = {vulnerability.id: vulnerability for vulnerability in group.advisory.vulnerabilities}
180
247
  for result in group.results:
181
248
  has_details = bool(result.atomic_results)
182
249
  if has_details:
183
- messages = f"**Detailed findings:** {self._atomic_summary(result)}"
250
+ findings = f"**Detailed findings:** {self._atomic_summary(result)}"
184
251
  if result.messages:
185
- messages += f"<br>**Overall evidence:** {self.safe_markdown('<br>'.join(result.messages))}"
252
+ findings += f"<br>**Overall evidence:** {self.format_findings(result)}"
186
253
  else:
187
- messages = self.safe_markdown("<br>".join(result.messages)) or "-"
188
- description = self.safe_markdown(result.description) or "-"
189
- self.mdfile.write(
190
- f"| {self.safe_markdown(result.name)} | {self.safe_markdown(result.test)} | {description} | - "
191
- f"| {self.format_advisory_result(result)} | {messages} |\n"
192
- )
254
+ findings = self.safe_markdown("<br>".join(result.messages)) or "-"
255
+ remediation = self.format_remediations(result)
256
+ self.mdfile.write(f"| {self.safe_markdown(result.name)} | - | {self.format_advisory_result(result)} | {findings} | {remediation} |\n")
193
257
  for index, atomic in enumerate(result.atomic_results):
194
258
  tree = "└──" if index == len(result.atomic_results) - 1 else "├──"
195
- atomic_description = self.safe_markdown(atomic.description) or "-"
196
- description = f"&nbsp;&nbsp;{tree}&nbsp;{atomic_description}"
197
259
  vulnerability_ids = _get_atomic_vulnerability_ids(atomic)
198
- vulnerabilities = self.safe_markdown(", ".join(vulnerability_ids)) if vulnerability_ids else "-"
199
- atomic_messages = self.safe_markdown("<br>".join(atomic.messages)) or "-"
200
- self.mdfile.write(f"| | | {description} | {vulnerabilities} | {self.format_advisory_result(atomic)} | {atomic_messages} |\n")
260
+ vulnerabilities = (
261
+ "<br>".join(
262
+ f"{SEVERITY_ICONS[vulnerability_by_id[vulnerability_id].severity]}&nbsp;{self.safe_markdown(vulnerability_id)}"
263
+ for vulnerability_id in vulnerability_ids
264
+ )
265
+ if vulnerability_ids
266
+ else "-"
267
+ )
268
+ atomic_findings = self.safe_markdown("<br>".join(atomic.messages)) or "-"
269
+ remediation = self.format_remediations(atomic)
270
+ self.mdfile.write(f"| &nbsp;&nbsp;{tree} | {vulnerabilities} | {self.format_advisory_result(atomic)} | {atomic_findings} | {remediation} |\n")
201
271
 
202
272
  def generate_section(self) -> None:
203
273
  """Generate detailed advisory metadata and findings."""
@@ -206,10 +276,11 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
206
276
  advisory = group.advisory
207
277
  anchor = f"sa-{advisory.sa_number.lower()}"
208
278
  title = self.safe_markdown(advisory.title)
209
- self.mdfile.write(f'### [SA{advisory.sa_number}: {title}]({advisory.url}) <a id="{anchor}"></a>\n\n')
279
+ self.mdfile.write(f'### {title} <a id="{anchor}"></a>\n\n')
210
280
  advisory_severity = group.severity
211
- severity = f"{SEVERITY_ICONS[advisory_severity]} **Severity:** {advisory_severity.value.title()}"
212
- self.mdfile.write(f"{severity}\n\n{self.safe_markdown(advisory.description)}\n\n")
281
+ severity = f"**Severity:** {SEVERITY_ICONS[advisory_severity]} {advisory_severity.value.title()}"
282
+ description = self.safe_markdown(advisory.description)
283
+ self.mdfile.write(f"> {severity}\\\n> **URL:** <{advisory.url}>\n>\n> {description}\n>\n")
213
284
  self._write_vulnerabilities(advisory)
214
285
  self._write_findings(group)
215
286
  if index < len(self.groups) - 1:
@@ -217,14 +288,11 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
217
288
  self.mdfile.write("\n")
218
289
 
219
290
 
220
- class SecurityAdvisoryRunOverview(SecurityAdvisoryMDReportBase):
291
+ class RunOverview(SecurityAdvisoryMDReportBase):
221
292
  """Generate the Run Overview section for a security advisory report."""
222
293
 
223
294
  ICON = "📋"
224
295
 
225
- _TABLE_COLUMNS: ClassVar[list[str]] = ["⚙️ Run Metric", "📝 Details"]
226
- TABLE_HEADING: ClassVar[list[str]] = MDReportBase.generate_table_heading(columns=_TABLE_COLUMNS)
227
-
228
296
  def _format_row_value(self, value: object) -> str:
229
297
  """Format one run overview value for Markdown table rendering."""
230
298
  if isinstance(value, list | tuple):
@@ -241,14 +309,25 @@ class SecurityAdvisoryRunOverview(SecurityAdvisoryMDReportBase):
241
309
  return "<br>".join(items)
242
310
  return self.safe_markdown(self.format_value(value))
243
311
 
244
- def generate_rows(self) -> Generator[str, None, None]:
245
- """Generate the rows for the security advisory run overview table."""
246
- run_overview = SecurityAdvisoryRunOverviewData.from_context(self.run_context)
247
- for label, value in run_overview.iter_rows():
248
- row_value = self._format_row_value(value)
249
- yield f"| **{label}** | {row_value} |\n"
250
-
251
312
  def generate_section(self) -> None:
252
- """Generate the security advisory run overview section."""
313
+ """Generate the Run Overview section."""
253
314
  self.write_heading(heading_level=2)
254
- self.write_table(table_heading=self.TABLE_HEADING, last_table=True)
315
+ run_overview = SecurityAdvisoryRunOverviewData.from_context(self.run_context)
316
+ start_time = self._format_row_value(run_overview.test_execution_start_time)
317
+ end_time = self._format_row_value(run_overview.test_execution_end_time)
318
+ duration = self._format_row_value(run_overview.total_duration)
319
+ overview_metrics: list[tuple[str, object]] = [
320
+ ("ANTA Version", run_overview.anta_version),
321
+ ("Duration", f"{duration} ({start_time} → {end_time})"),
322
+ ("Security Advisories Tested", run_overview.security_advisories_assessed),
323
+ ("Total Devices In Inventory", run_overview.total_devices_in_inventory),
324
+ ("Devices Assessed", run_overview.devices_assessed),
325
+ ("Devices Unreachable At Setup", run_overview.devices_unreachable_at_setup),
326
+ ("Devices Filtered At Setup", run_overview.devices_filtered_at_setup),
327
+ ("Filters Applied", run_overview.filters_applied),
328
+ ]
329
+ if run_overview.warnings_at_setup:
330
+ overview_metrics.append(("Warnings At Setup", run_overview.warnings_at_setup))
331
+ self.mdfile.write("| | |\n| :- | :- |\n")
332
+ for label, value in overview_metrics:
333
+ self.mdfile.write(f"| **{label}** | {self._format_row_value(value)} |\n")
@@ -220,19 +220,19 @@ def generate_security_advisory_md_report(
220
220
  validate_advisory_results(run_context.manager.results)
221
221
 
222
222
  from anta._advisory.reporter.md_reporter import ( # noqa: PLC0415
223
- AdvisoryExposureSummary,
223
+ AdvisoryAssessmentSummary,
224
224
  ANTASecurityAdvisoryReport,
225
+ RunOverview,
225
226
  SecurityAdvisoryDetails,
226
- SecurityAdvisoryRunOverview,
227
227
  )
228
228
 
229
229
  try:
230
230
  with md_filename.open("w", encoding="utf-8") as mdfile:
231
231
  ANTASecurityAdvisoryReport(mdfile, report, config, run_context).generate_section()
232
232
  if report.groups:
233
- AdvisoryExposureSummary(mdfile, report, config, run_context).generate_section()
233
+ AdvisoryAssessmentSummary(mdfile, report, config, run_context).generate_section()
234
234
  SecurityAdvisoryDetails(mdfile, report, config, run_context).generate_section()
235
- SecurityAdvisoryRunOverview(mdfile, report, config, run_context).generate_section()
235
+ RunOverview(mdfile, report, config, run_context).generate_section()
236
236
  except OSError as exc:
237
237
  message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'."
238
238
  anta_log_exception(exc, message, logger)
@@ -86,6 +86,7 @@ UNSUPPORTED_PLATFORM_ERRORS = [
86
86
  "Invalid input (at token 1: 'supervisor-peer:/mnt/flash')",
87
87
  "Incomplete command (at token 1: 'module')",
88
88
  "Incomplete command (at token 1: 'ptp')",
89
+ "Invalid input (at token 1: 'directflow')",
89
90
  ]
90
91
  """Error messages indicating platform or hardware unsupported commands. Includes both general hardware
91
92
  platform errors and specific ASIC family limitations.
@@ -62,7 +62,7 @@ ADVISORY = _AdvisoryMetadata(
62
62
  _AdvisoryVulnerability(
63
63
  id="CVE-2025-0936",
64
64
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
65
- description=("CVE-2025-0936: gNOI TransferToRemote credential exposure through OpenConfig accounting or tracing."),
65
+ description=("gNOI TransferToRemote credential exposure through OpenConfig accounting or tracing."),
66
66
  ),
67
67
  ),
68
68
  url=("https://www.arista.com/en/support/advisories-notices/security-advisory/21394-security-advisory-0117"),
@@ -75,16 +75,32 @@ ADVISORY = _AdvisoryMetadata(
75
75
  )
76
76
 
77
77
 
78
- def _evaluate_gnmi_transport_enabled(gnmi_output: Mapping[str, object]) -> bool | None:
79
- """Return whether at least one gNMI transport is enabled."""
78
+ def _gnmi_transport_values(gnmi_output: Mapping[str, object]) -> tuple[object, ...] | None:
79
+ """Return transport values from nested or flattened EOS gNMI output.
80
+
81
+ EOS 4.33 eAPI revision 1 exposes a single transport at the top level, while
82
+ newer schemas expose named transports below ``transports``.
83
+ """
84
+ if "transports" not in gnmi_output:
85
+ enabled = gnmi_output.get("enabled")
86
+ if not isinstance(enabled, bool):
87
+ return None
88
+ return (gnmi_output,) if enabled else ()
89
+
80
90
  transports = gnmi_output.get("transports")
81
91
  if not isinstance(transports, Mapping):
82
92
  return None
83
- if not transports:
84
- return False
93
+ return tuple(transports.values())
94
+
95
+
96
+ def _evaluate_gnmi_transport_enabled(gnmi_output: Mapping[str, object]) -> bool | None:
97
+ """Return whether at least one gNMI transport is enabled."""
98
+ transports = _gnmi_transport_values(gnmi_output)
99
+ if transports is None:
100
+ return None
85
101
 
86
102
  unknown = False
87
- for transport in transports.values():
103
+ for transport in transports:
88
104
  if not isinstance(transport, Mapping):
89
105
  unknown = True
90
106
  continue
@@ -99,12 +115,12 @@ def _evaluate_gnmi_transport_enabled(gnmi_output: Mapping[str, object]) -> bool
99
115
 
100
116
  def _evaluate_gnmi_accounting_enabled(gnmi_output: Mapping[str, object]) -> bool | None:
101
117
  """Return whether an enabled gNMI transport has accounting enabled."""
102
- transports = gnmi_output.get("transports")
103
- if not isinstance(transports, Mapping):
118
+ transports = _gnmi_transport_values(gnmi_output)
119
+ if transports is None:
104
120
  return None
105
121
 
106
122
  unknown = False
107
- for transport in transports.values():
123
+ for transport in transports:
108
124
  if not isinstance(transport, Mapping):
109
125
  unknown = True
110
126
  continue
@@ -273,7 +289,7 @@ class VerifySA117(OptionalCommandsMixin, _AntaAdvisoryTest):
273
289
  )
274
290
  vulnerability = ADVISORY.vulnerabilities[0]
275
291
  atomic_result = self.result.add(
276
- vulnerability.description,
292
+ f"Verify {vulnerability.id}.",
277
293
  vulnerability_ids=(vulnerability.id,),
278
294
  )
279
295
  project_advisory_status(atomic_result, status, message, remediation)
@@ -53,7 +53,7 @@ ADVISORY = _AdvisoryMetadata(
53
53
  _AdvisoryVulnerability(
54
54
  id="CVE-2026-10040",
55
55
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
56
- description="CVE-2026-10040: Secure Boot Software Image verification bypass.",
56
+ description="Secure Boot Software Image verification bypass.",
57
57
  ),
58
58
  ),
59
59
  url=("https://www.arista.com/en/support/advisories-notices/security-advisory/24074-security-advisory-0140"),
@@ -163,7 +163,7 @@ class VerifySA140(_AntaAdvisoryTest):
163
163
  )
164
164
  vulnerability = ADVISORY.vulnerabilities[0]
165
165
  atomic_result = self.result.add(
166
- vulnerability.description,
166
+ f"Verify {vulnerability.id}.",
167
167
  vulnerability_ids=(vulnerability.id,),
168
168
  )
169
169
  project_advisory_status(atomic_result, status, message, remediation)
@@ -226,7 +226,7 @@ ADVISORY = _AdvisoryMetadata(
226
226
  _AdvisoryVulnerability(
227
227
  id="CVE-2026-12546",
228
228
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
229
- description=("CVE-2026-12546: Next-hop redirection bypass for packets requiring exception handling."),
229
+ description=("Next-hop redirection bypass for packets requiring exception handling."),
230
230
  ),
231
231
  ),
232
232
  url=("https://www.arista.com/en/support/advisories-notices/security-advisory/24111-security-advisory-0142"),
@@ -732,7 +732,7 @@ class VerifySA142(OptionalCommandsMixin, _AntaAdvisoryTest):
732
732
  )
733
733
  vulnerability = ADVISORY.vulnerabilities[0]
734
734
  atomic_result = self.result.add(
735
- vulnerability.description,
735
+ f"Verify {vulnerability.id}.",
736
736
  vulnerability_ids=(vulnerability.id,),
737
737
  )
738
738
  project_advisory_status(atomic_result, status, message, remediation)
@@ -474,7 +474,7 @@ class VerifySA146(OptionalCommandsMixin, _AntaAdvisoryTest):
474
474
  AntaCommand(command="show version detail", revision=1),
475
475
  OptionalAntaCommand(command="show management api gnmi", revision=1),
476
476
  OptionalAntaCommand(command="show management api gribi", revision=1),
477
- OptionalAntaCommand(command="show daemon TerminAttr", revision=1),
477
+ OptionalAntaCommand(command="show daemon", revision=1),
478
478
  OptionalAntaCommand(command="show running-config section grpcaddr", ofmt="text"),
479
479
  OptionalAntaCommand(command="show management security ssl profile", revision=1),
480
480
  ]
@@ -517,7 +517,7 @@ class VerifySA146(OptionalCommandsMixin, _AntaAdvisoryTest):
517
517
  )
518
518
  vulnerability = ADVISORY.vulnerabilities[0]
519
519
  atomic_result = self.result.add(
520
- vulnerability.description,
520
+ f"Verify {vulnerability.id}.",
521
521
  vulnerability_ids=(vulnerability.id,),
522
522
  )
523
523
  project_advisory_status(atomic_result, status, message, remediation)
@@ -61,22 +61,22 @@ ADVISORY = _AdvisoryMetadata(
61
61
  _AdvisoryVulnerability(
62
62
  id="CVE-2026-59995",
63
63
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
64
- description="CVE-2026-59995: SFTP client issue when connecting to an untrusted server.",
64
+ description="SFTP client issue when connecting to an untrusted server.",
65
65
  ),
66
66
  _AdvisoryVulnerability(
67
67
  id="CVE-2026-59996",
68
68
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
69
- description=("CVE-2026-59996: SCP remote-to-remote client issue involving an untrusted server."),
69
+ description=("SCP remote-to-remote client issue involving an untrusted server."),
70
70
  ),
71
71
  _AdvisoryVulnerability(
72
72
  id="CVE-2026-60001",
73
73
  severity=_AdvisoryVulnerabilitySeverity.MEDIUM,
74
- description="CVE-2026-60001: OpenSSH server issue affecting accepted SSH connections.",
74
+ description="OpenSSH server issue affecting accepted SSH connections.",
75
75
  ),
76
76
  _AdvisoryVulnerability(
77
77
  id="CVE-2026-60002",
78
78
  severity=_AdvisoryVulnerabilitySeverity.CRITICAL,
79
- description=("CVE-2026-60002: SSH client issue when connecting to a malicious or compromised server."),
79
+ description=("SSH client issue when connecting to a malicious or compromised server."),
80
80
  ),
81
81
  ),
82
82
  url=("https://www.arista.com/en/support/advisories-notices/security-advisory/24515-security-advisory-0147"),
@@ -428,7 +428,7 @@ class VerifySA147(OptionalCommandsMixin, _AntaAdvisoryTest):
428
428
  )
429
429
  for vulnerability, (status, message, remediation) in zip(ADVISORY.vulnerabilities, assessments, strict=True):
430
430
  atomic_result = self.result.add(
431
- vulnerability.description,
431
+ f"Verify {vulnerability.id}.",
432
432
  vulnerability_ids=(vulnerability.id,),
433
433
  )
434
434
  project_advisory_status(atomic_result, status, message, remediation)
@@ -316,7 +316,10 @@ class VerifyStpTopologyChanges(AntaTest):
316
316
  # Verifies the number of changes across all interfaces
317
317
  for topology, topology_details in stp_topologies.items():
318
318
  for interface, details in topology_details.get("interfaces", {}).items():
319
- if (num_of_changes := details.get("numChanges")) > self.inputs.threshold:
319
+ num_of_changes = details.get("numChanges")
320
+ if num_of_changes is None:
321
+ self.result.is_failure(f"Topology: {topology} Interface: {interface} - Number of changes counter not found")
322
+ elif num_of_changes > self.inputs.threshold:
320
323
  self.result.is_failure(
321
324
  f"Topology: {topology} Interface: {interface} - Number of changes not within the threshold - Expected: "
322
325
  f"{self.inputs.threshold} Actual: {num_of_changes}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anta
3
- Version: 1.10.0.dev1
3
+ Version: 1.10.0.dev3
4
4
  Summary: Arista Network Test Automation (ANTA) Framework
5
5
  Author-email: Arista Networks ANTA maintainers <anta-dev@arista.com>
6
6
  Maintainer-email: Arista Networks ANTA maintainers <anta-dev@arista.com>, Khelil Sator <ksator@arista.com>, Matthieu Tâche <mtache@arista.com>, Thomas Grimonet <tgrimonet@arista.com>, Guillaume Mulocher <gmulocher@arista.com>, Carl Baillargeon <carl.baillargeon@arista.com>
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "anta"
8
- version = "v1.10.0.dev1"
8
+ version = "v1.10.0.dev3"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Arista Networks ANTA maintainers", email = "anta-dev@arista.com" }]
11
11
  maintainers = [
@@ -94,7 +94,7 @@ doc = [
94
94
  "griffe >=2.2.0,<2.3",
95
95
  "griffe-warnings-deprecated~=1.1.1",
96
96
  "mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac",
97
- "zensical==0.0.56",
97
+ "zensical==0.0.57",
98
98
  "mkdocs~=1.6",
99
99
  "mkdocs-autorefs~=1.4",
100
100
  "mkdocs-material~=9.7",
@@ -121,7 +121,7 @@ namespaces = false
121
121
  # Version
122
122
  ################################
123
123
  [tool.bumpver]
124
- current_version = "1.10.0.dev1"
124
+ current_version = "1.10.0.dev3"
125
125
  version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
126
126
  commit_message = "bump: Version {old_version} -> {new_version}"
127
127
  commit = true
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes