anta 1.10.0.dev1__tar.gz → 1.10.0.dev2__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.dev2}/PKG-INFO +1 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/reporter/md_reporter.py +96 -49
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/reporter/reporting.py +4 -4
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/constants.py +1 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/sa_117.py +24 -8
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/sa_146.py +1 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/stp.py +4 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/PKG-INFO +1 -1
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/pyproject.toml +3 -3
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/LICENSE +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/README.md +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/base.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/eos_versions.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/optional_commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/platforms.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/remediation.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/reporter/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/reporter/csv_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/results.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_advisory/status.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_eos/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_eos/version.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/_runner.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/catalog.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/_main.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/check/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/check/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/console.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/debug/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/debug/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/debug/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/exec/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/exec/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/exec/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/get/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/get/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/get/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/nrfu/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/nrfu/commands.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/nrfu/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/psirt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/cli/utils.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/custom_types.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/decorators.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/device.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/avt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/bfd.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/configuration.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/connectivity.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/cvx.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/evpn.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/flow_tracking.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/hardware.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/interfaces.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/logging.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/path_selection.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/routing/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/routing/bgp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/routing/generic.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/routing/isis.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/routing/ospf.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/security.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/services.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/snmp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/stun.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/system.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/input_models/vlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/inventory/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/inventory/exceptions.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/inventory/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/logger.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/py.typed +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/reporter/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/reporter/csv_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/reporter/jinja_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/reporter/md_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/reporter/table_reporter.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/result_manager/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/result_manager/models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/runner.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/settings.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/aaa.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/sa_140.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/sa_142.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/advisories/sa_147.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/avt.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/bfd.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/configuration.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/connectivity.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/cvx.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/evpn.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/field_notices.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/flow_tracking.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/greent.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/hardware.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/interfaces.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/lanz.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/logging.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/mlag.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/multicast.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/path_selection.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/profiles.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/ptp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/routing/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/routing/bgp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/routing/generic.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/routing/isis.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/routing/ospf.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/security.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/services.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/snmp.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/software.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/stun.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/system.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/vlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tests/vxlan.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta/tools.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/SOURCES.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/dependency_links.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/entry_points.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/requires.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/anta.egg-info/top_level.txt +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/__init__.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/_auth.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/_constants.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/_errors.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/_models.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/_types.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/aio_portcheck.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/config_session.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/device.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/asynceapi/errors.py +0 -0
- {anta-1.10.0.dev1 → anta-1.10.0.dev2}/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.dev2
|
|
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>
|
|
@@ -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
|
|
|
@@ -64,6 +64,27 @@ 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
|
+
if atomic_remediations:
|
|
85
|
+
return "<br>".join(f"• {self.safe_markdown(remediation)}" for remediation in unique_remediations)
|
|
86
|
+
return self.safe_markdown("\n".join(unique_remediations)) or "-"
|
|
87
|
+
|
|
67
88
|
|
|
68
89
|
class ANTASecurityAdvisoryReport(SecurityAdvisoryMDReportBase):
|
|
69
90
|
"""Generate the title and table of contents for a security advisory report."""
|
|
@@ -72,15 +93,18 @@ class ANTASecurityAdvisoryReport(SecurityAdvisoryMDReportBase):
|
|
|
72
93
|
|
|
73
94
|
def generate_section(self) -> None:
|
|
74
95
|
"""Generate the security advisory report heading and table of contents."""
|
|
75
|
-
self.
|
|
76
|
-
toc = "**Table of Contents:**\n\n
|
|
96
|
+
self.mdfile.write('<h1 id="anta-security-advisory-report" align="center">🛡️ ANTA Security Advisory Report 🛡️</h1>\n\n')
|
|
97
|
+
toc = "**Table of Contents:**\n\n"
|
|
77
98
|
if self.groups:
|
|
78
|
-
toc += "
|
|
79
|
-
|
|
99
|
+
toc += "- [Advisory Assessment Summary](#advisory-assessment-summary)\n- [Security Advisory Details](#security-advisory-details)\n"
|
|
100
|
+
for group in self.groups:
|
|
101
|
+
advisory = group.advisory
|
|
102
|
+
toc += f" - [SA{advisory.sa_number}: {self.safe_markdown(advisory.title)}](#sa-{advisory.sa_number.lower()})\n"
|
|
103
|
+
toc += "- [Run Overview](#run-overview)"
|
|
80
104
|
self.mdfile.write(toc + "\n\n")
|
|
81
105
|
|
|
82
106
|
|
|
83
|
-
class
|
|
107
|
+
class AdvisoryAssessmentSummary(SecurityAdvisoryMDReportBase):
|
|
84
108
|
"""Generate a compact status summary grouped by security advisory."""
|
|
85
109
|
|
|
86
110
|
ICON = "📊"
|
|
@@ -107,8 +131,7 @@ class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
|
|
|
107
131
|
for group in self.groups:
|
|
108
132
|
advisory = group.advisory
|
|
109
133
|
advisory_link = f"[SA{advisory.sa_number}: {self.safe_markdown(advisory.title)}](#sa-{advisory.sa_number.lower()})"
|
|
110
|
-
|
|
111
|
-
severity = f"{SEVERITY_ICONS[advisory_severity]} {advisory_severity.value.title()}"
|
|
134
|
+
severity = self.format_severity(group.severity)
|
|
112
135
|
devices = len({result.name for result in group.results})
|
|
113
136
|
yield (
|
|
114
137
|
f"| {advisory_link} | {severity} | {devices} "
|
|
@@ -121,7 +144,7 @@ class AdvisoryExposureSummary(SecurityAdvisoryMDReportBase):
|
|
|
121
144
|
)
|
|
122
145
|
|
|
123
146
|
def generate_section(self) -> None:
|
|
124
|
-
"""Generate the advisory
|
|
147
|
+
"""Generate the advisory assessment summary section."""
|
|
125
148
|
self.write_heading(heading_level=2)
|
|
126
149
|
self.write_table(table_heading=self.TABLE_HEADING)
|
|
127
150
|
|
|
@@ -133,29 +156,34 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
|
|
|
133
156
|
|
|
134
157
|
def _write_vulnerabilities(self, advisory: _AdvisoryMetadata) -> None:
|
|
135
158
|
"""Write vulnerability details for an advisory."""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
self.
|
|
139
|
-
for
|
|
159
|
+
# NOTE: Nested tables are not supported consistently by every Markdown renderer. Prefix every table line and pad it with quoted blank lines to maximize
|
|
160
|
+
# compatibility across renderers that support Markdown tables.
|
|
161
|
+
heading = self.generate_table_heading(["Vulnerability", "Severity", "Description"])
|
|
162
|
+
self.mdfile.write("\n".join(f"> {line}" for line in heading) + "\n")
|
|
163
|
+
vulnerabilities = sorted(
|
|
164
|
+
advisory.vulnerabilities,
|
|
165
|
+
key=lambda vulnerability: (-_ADVISORY_VULNERABILITY_SEVERITY_RANK[vulnerability.severity], vulnerability.id.casefold()),
|
|
166
|
+
)
|
|
167
|
+
for vulnerability in vulnerabilities:
|
|
140
168
|
vulnerability_id = self.safe_markdown(vulnerability.id)
|
|
141
169
|
description = self.safe_markdown(vulnerability.description)
|
|
142
|
-
self.mdfile.write(f"| {vulnerability_id} | {
|
|
143
|
-
self.mdfile.write("\n")
|
|
170
|
+
self.mdfile.write(f"> | {vulnerability_id} | {self.format_severity(vulnerability.severity)} | {description} |\n")
|
|
171
|
+
self.mdfile.write(">\n\n")
|
|
144
172
|
|
|
145
173
|
def _write_findings(self, group: AdvisoryResultGroup) -> None:
|
|
146
174
|
"""Write per-device findings for an advisory."""
|
|
147
175
|
# 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
176
|
self.mdfile.write("#### 🔎 Device Findings\n\n")
|
|
150
177
|
if self.config.expand_results:
|
|
151
178
|
self._write_expanded_findings(group)
|
|
152
179
|
return
|
|
153
180
|
|
|
154
|
-
heading = self.generate_table_heading(["Device", "
|
|
181
|
+
heading = self.generate_table_heading(["Device", "Result", "Findings", "Remediations"])
|
|
155
182
|
self.mdfile.write("\n".join(heading) + "\n")
|
|
156
183
|
for result in group.results:
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
findings = self.safe_markdown("<br>".join(result.messages)) or "-"
|
|
185
|
+
remediation = self.format_remediations(result)
|
|
186
|
+
self.mdfile.write(f"| {self.safe_markdown(result.name)} | {self.format_advisory_result(result)} | {findings} | {remediation} |\n")
|
|
159
187
|
|
|
160
188
|
@staticmethod
|
|
161
189
|
def _atomic_summary(result: TestResult) -> str:
|
|
@@ -175,29 +203,39 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
|
|
|
175
203
|
|
|
176
204
|
def _write_expanded_findings(self, group: AdvisoryResultGroup) -> None:
|
|
177
205
|
"""Write parent advisory results followed by their actual detailed issue results."""
|
|
178
|
-
heading = self.generate_table_heading(["Device", "
|
|
206
|
+
heading = self.generate_table_heading(["Device", "Description", "Vulnerability ID(s)", "Result", "Findings", "Remediations"])
|
|
179
207
|
self.mdfile.write("\n".join(heading) + "\n")
|
|
208
|
+
vulnerability_by_id = {vulnerability.id: vulnerability for vulnerability in group.advisory.vulnerabilities}
|
|
180
209
|
for result in group.results:
|
|
181
210
|
has_details = bool(result.atomic_results)
|
|
182
211
|
if has_details:
|
|
183
|
-
|
|
212
|
+
findings = f"**Detailed findings:** {self._atomic_summary(result)}"
|
|
184
213
|
if result.messages:
|
|
185
|
-
|
|
214
|
+
findings += f"<br>**Overall evidence:** {self.safe_markdown('<br>'.join(result.messages))}"
|
|
186
215
|
else:
|
|
187
|
-
|
|
216
|
+
findings = self.safe_markdown("<br>".join(result.messages)) or "-"
|
|
188
217
|
description = self.safe_markdown(result.description) or "-"
|
|
189
|
-
self.
|
|
190
|
-
|
|
191
|
-
f"| {self.format_advisory_result(result)} | {messages} |\n"
|
|
192
|
-
)
|
|
218
|
+
remediation = self.format_remediations(result)
|
|
219
|
+
self.mdfile.write(f"| {self.safe_markdown(result.name)} | {description} | - | {self.format_advisory_result(result)} | {findings} | {remediation} |\n")
|
|
193
220
|
for index, atomic in enumerate(result.atomic_results):
|
|
194
221
|
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
222
|
vulnerability_ids = _get_atomic_vulnerability_ids(atomic)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
if vulnerability_ids:
|
|
224
|
+
atomic_description = "<br>".join(self.safe_markdown(vulnerability_by_id[vulnerability_id].description) for vulnerability_id in vulnerability_ids)
|
|
225
|
+
else:
|
|
226
|
+
atomic_description = self.safe_markdown(atomic.description) or "-"
|
|
227
|
+
description = f" {tree} {atomic_description}"
|
|
228
|
+
vulnerabilities = (
|
|
229
|
+
"<br>".join(
|
|
230
|
+
f"{SEVERITY_ICONS[vulnerability_by_id[vulnerability_id].severity]} {self.safe_markdown(vulnerability_id)}"
|
|
231
|
+
for vulnerability_id in vulnerability_ids
|
|
232
|
+
)
|
|
233
|
+
if vulnerability_ids
|
|
234
|
+
else "-"
|
|
235
|
+
)
|
|
236
|
+
atomic_findings = self.safe_markdown("<br>".join(atomic.messages)) or "-"
|
|
237
|
+
remediation = self.format_remediations(atomic)
|
|
238
|
+
self.mdfile.write(f"| | {description} | {vulnerabilities} | {self.format_advisory_result(atomic)} | {atomic_findings} | {remediation} |\n")
|
|
201
239
|
|
|
202
240
|
def generate_section(self) -> None:
|
|
203
241
|
"""Generate detailed advisory metadata and findings."""
|
|
@@ -206,10 +244,11 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
|
|
|
206
244
|
advisory = group.advisory
|
|
207
245
|
anchor = f"sa-{advisory.sa_number.lower()}"
|
|
208
246
|
title = self.safe_markdown(advisory.title)
|
|
209
|
-
self.mdfile.write(f'###
|
|
247
|
+
self.mdfile.write(f'### SA{advisory.sa_number}: {title} <a id="{anchor}"></a>\n\n')
|
|
210
248
|
advisory_severity = group.severity
|
|
211
|
-
severity = f"{SEVERITY_ICONS[advisory_severity]}
|
|
212
|
-
self.
|
|
249
|
+
severity = f"**Severity:** {SEVERITY_ICONS[advisory_severity]} {advisory_severity.value.title()}"
|
|
250
|
+
description = self.safe_markdown(advisory.description)
|
|
251
|
+
self.mdfile.write(f"> {severity}\\\n> **URL:** <{advisory.url}>\n>\n> {description}\n>\n")
|
|
213
252
|
self._write_vulnerabilities(advisory)
|
|
214
253
|
self._write_findings(group)
|
|
215
254
|
if index < len(self.groups) - 1:
|
|
@@ -217,14 +256,11 @@ class SecurityAdvisoryDetails(SecurityAdvisoryMDReportBase):
|
|
|
217
256
|
self.mdfile.write("\n")
|
|
218
257
|
|
|
219
258
|
|
|
220
|
-
class
|
|
259
|
+
class RunOverview(SecurityAdvisoryMDReportBase):
|
|
221
260
|
"""Generate the Run Overview section for a security advisory report."""
|
|
222
261
|
|
|
223
262
|
ICON = "📋"
|
|
224
263
|
|
|
225
|
-
_TABLE_COLUMNS: ClassVar[list[str]] = ["⚙️ Run Metric", "📝 Details"]
|
|
226
|
-
TABLE_HEADING: ClassVar[list[str]] = MDReportBase.generate_table_heading(columns=_TABLE_COLUMNS)
|
|
227
|
-
|
|
228
264
|
def _format_row_value(self, value: object) -> str:
|
|
229
265
|
"""Format one run overview value for Markdown table rendering."""
|
|
230
266
|
if isinstance(value, list | tuple):
|
|
@@ -241,14 +277,25 @@ class SecurityAdvisoryRunOverview(SecurityAdvisoryMDReportBase):
|
|
|
241
277
|
return "<br>".join(items)
|
|
242
278
|
return self.safe_markdown(self.format_value(value))
|
|
243
279
|
|
|
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
280
|
def generate_section(self) -> None:
|
|
252
|
-
"""Generate the
|
|
281
|
+
"""Generate the Run Overview section."""
|
|
253
282
|
self.write_heading(heading_level=2)
|
|
254
|
-
|
|
283
|
+
run_overview = SecurityAdvisoryRunOverviewData.from_context(self.run_context)
|
|
284
|
+
start_time = self._format_row_value(run_overview.test_execution_start_time)
|
|
285
|
+
end_time = self._format_row_value(run_overview.test_execution_end_time)
|
|
286
|
+
duration = self._format_row_value(run_overview.total_duration)
|
|
287
|
+
overview_metrics: list[tuple[str, object]] = [
|
|
288
|
+
("ANTA Version", run_overview.anta_version),
|
|
289
|
+
("Duration", f"{duration} ({start_time} → {end_time})"),
|
|
290
|
+
("Security Advisories Tested", run_overview.security_advisories_assessed),
|
|
291
|
+
("Total Devices In Inventory", run_overview.total_devices_in_inventory),
|
|
292
|
+
("Devices Assessed", run_overview.devices_assessed),
|
|
293
|
+
("Devices Unreachable At Setup", run_overview.devices_unreachable_at_setup),
|
|
294
|
+
("Devices Filtered At Setup", run_overview.devices_filtered_at_setup),
|
|
295
|
+
("Filters Applied", run_overview.filters_applied),
|
|
296
|
+
]
|
|
297
|
+
if run_overview.warnings_at_setup:
|
|
298
|
+
overview_metrics.append(("Warnings At Setup", run_overview.warnings_at_setup))
|
|
299
|
+
self.mdfile.write("| | |\n| :- | :- |\n")
|
|
300
|
+
for label, value in overview_metrics:
|
|
301
|
+
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.
|
|
@@ -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
|
|
@@ -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
|
]
|
|
@@ -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.dev2
|
|
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.dev2"
|
|
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.dev2"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|