From 060e0c6de3c309053f913363b1d16df24408431b Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Mon, 26 Oct 2026 09:57:21 +0100 Subject: [PATCH] Deduplicate updated vulnerable dependencies --- doc/changes/unreleased.md | 9 +++--- .../dependencies/track_vulnerabilities.py | 5 +++- test/unit/tool_template_test.py | 2 +- .../track_vulnerabilities_test.py | 30 +++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 91e25e0a0..24533cfb9 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -1,5 +1,10 @@ # Unreleased +## Bugfixes + +* #965: Added additional parameters to ``format:fix`` tools to run with the minimum Python version +* #983: Deduplicated vulnerable dependencies in Nox session ``release:prepare`` + ## Features * #940: Added shared validation for packaged agent skills and the `skills:check` Nox session. @@ -14,7 +19,3 @@ * #972: Switched from ravsamhq/notify-slack-action to maintained slackapi/slack-github-action & removed workflow versioned header * #967: Refactored skill installation into reusable filesystem helpers. * #965: Removed support for Python 3.10 (reach end-of-life October 2026), updated to Ubuntu-26.04 for GitHub actions, and updated default databases - -## Bugfixes - -* #965: Added additional parameters to ``format:fix`` tools to run with the minimum Python version diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index 37d72da31..5032c52ef 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -80,5 +80,8 @@ def formatted(vuln: Vulnerability) -> str: ) return f'| {" | ".join(columns)} |' - body = "\n".join(formatted(v) for v in resolved) + # pip-audit can return the same vulnerability more than once. Keep + # distinct table rows, but only render each row once. + rows = sorted({formatted(vuln) for vuln in resolved}) + body = "\n".join(rows) return f"{header}\n{body}" diff --git a/test/unit/tool_template_test.py b/test/unit/tool_template_test.py index e09145044..ffe176dcc 100644 --- a/test/unit/tool_template_test.py +++ b/test/unit/tool_template_test.py @@ -60,5 +60,5 @@ def test_retrieve_templates(subpackage, expected): def test_install_templates(templates, pkg, expected, tmp_path): template.install_template(templates, tmp_path, pkg) actual = {file.name for file in tmp_path.iterdir()} - expected = {name for name in expected} + expected = {*expected} assert actual == expected diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 71e1ed51a..2a16a42b4 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -118,6 +118,36 @@ def test_resolved_vulnerability(self, sample_vulnerability): ) assert audit.resolved_vulnerabilities == [sample_vulnerability.vulnerability] + def test_report_deduplicates_repeated_vulnerability_rows( + self, sample_vulnerability + ): + vulnerability = sample_vulnerability.vulnerability + earlier_alphabetically = Vulnerability( + package={"name": "black", "version": "26.5.1"}, + id="CVE-2026-114134", + aliases=[], + fix_versions=["26.10.1"], + description="Dummy description", + ) + audit = DependenciesAudit( + previous_vulnerabilities=[ + vulnerability, + earlier_alphabetically, + vulnerability, + earlier_alphabetically, + ], + current_vulnerabilities=[], + ) + + assert audit.report_resolved_vulnerabilities() == ( + "This release fixes vulnerabilities by updating dependencies:\n\n" + "| Dependency | Vulnerability | Affected | Fixed in |\n" + "|------------|---------------|----------|----------|\n" + "| black | CVE-2026-114134 | 26.5.1 | 26.10.1 |\n" + f"| {sample_vulnerability.package_name} | {sample_vulnerability.cve_id} | " + f"{sample_vulnerability.version} | {sample_vulnerability.fix_version} |" + ) + def test_vulnerability_without_fix_version_is_not_reported_as_resolved( self, sample_vulnerability ):