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.
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/PKG-INFO +1 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/csv_reporter.py +3 -3
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/md_reporter.py +132 -53
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/reporting.py +4 -4
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/constants.py +1 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_117.py +26 -10
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_140.py +2 -2
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_142.py +2 -2
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_146.py +2 -2
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/sa_147.py +5 -5
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/stp.py +4 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/PKG-INFO +1 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/pyproject.toml +3 -3
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/LICENSE +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/README.md +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/base.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/eos_versions.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/optional_commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/platforms.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/remediation.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/reporter/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/results.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_advisory/status.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_eos/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_eos/version.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/_runner.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/catalog.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/_main.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/check/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/check/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/console.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/debug/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/exec/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/get/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/nrfu/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/psirt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/cli/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/custom_types.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/decorators.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/device.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/avt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/bfd.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/configuration.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/connectivity.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/cvx.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/evpn.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/flow_tracking.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/hardware.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/interfaces.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/logging.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/path_selection.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/bgp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/generic.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/isis.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/routing/ospf.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/security.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/services.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/snmp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/stun.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/system.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/input_models/vlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/exceptions.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/inventory/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/logger.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/py.typed +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/csv_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/jinja_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/md_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/reporter/table_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/result_manager/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/result_manager/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/runner.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/settings.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/aaa.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/advisories/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/avt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/bfd.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/configuration.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/connectivity.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/cvx.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/evpn.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/field_notices.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/flow_tracking.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/greent.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/hardware.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/interfaces.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/lanz.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/logging.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/mlag.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/multicast.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/path_selection.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/profiles.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/ptp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/bgp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/generic.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/isis.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/routing/ospf.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/security.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/services.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/snmp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/software.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/stun.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/system.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/vlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tests/vxlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta/tools.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/SOURCES.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/dependency_links.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/entry_points.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/requires.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/anta.egg-info/top_level.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_auth.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_constants.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_errors.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/_types.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/aio_portcheck.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/config_session.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/device.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev3}/asynceapi/errors.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
73
|
+
cls.split_list_to_txt_list(result.messages, "\\n"),
|
|
74
74
|
cls._format_result(row_result),
|
|
75
|
-
|
|
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]} {_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]} {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"• {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.
|
|
76
|
-
toc = "**Table of Contents:**\n\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 += "
|
|
79
|
-
|
|
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
|
|
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
|
-
"
|
|
153
|
+
"🛑 Affected",
|
|
92
154
|
"❓ Inconclusive",
|
|
93
155
|
"✅ Mitigated",
|
|
94
156
|
"✅ 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"[
|
|
110
|
-
|
|
111
|
-
severity = f"{SEVERITY_ICONS[advisory_severity]} {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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
self.
|
|
139
|
-
for
|
|
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} | {
|
|
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", "
|
|
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
|
-
|
|
158
|
-
|
|
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", "
|
|
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
|
-
|
|
250
|
+
findings = f"**Detailed findings:** {self._atomic_summary(result)}"
|
|
184
251
|
if result.messages:
|
|
185
|
-
|
|
252
|
+
findings += f"<br>**Overall evidence:** {self.format_findings(result)}"
|
|
186
253
|
else:
|
|
187
|
-
|
|
188
|
-
|
|
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" {tree} {atomic_description}"
|
|
197
259
|
vulnerability_ids = _get_atomic_vulnerability_ids(atomic)
|
|
198
|
-
vulnerabilities =
|
|
199
|
-
|
|
200
|
-
|
|
260
|
+
vulnerabilities = (
|
|
261
|
+
"<br>".join(
|
|
262
|
+
f"{SEVERITY_ICONS[vulnerability_by_id[vulnerability_id].severity]} {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"| {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'###
|
|
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]}
|
|
212
|
-
self.
|
|
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
|
|
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
|
|
313
|
+
"""Generate the Run Overview section."""
|
|
253
314
|
self.write_heading(heading_level=2)
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
AdvisoryAssessmentSummary(mdfile, report, config, run_context).generate_section()
|
|
234
234
|
SecurityAdvisoryDetails(mdfile, report, config, run_context).generate_section()
|
|
235
|
-
|
|
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=("
|
|
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
|
|
79
|
-
"""Return
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
103
|
-
if
|
|
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
|
|
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.
|
|
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="
|
|
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.
|
|
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=("
|
|
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.
|
|
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
|
|
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.
|
|
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="
|
|
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=("
|
|
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="
|
|
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=("
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|