From 2ba062ba94c6b6bb60145679691ad308664265f4 Mon Sep 17 00:00:00 2001 From: ckunki Date: Sun, 5 Apr 2026 14:18:13 +0200 Subject: [PATCH 01/23] #402: Created nox task to detect resolved GitHub security issues --- doc/changes/unreleased.md | 1 + exasol/toolbox/nox/_dependencies.py | 19 +++- exasol/toolbox/util/dependencies/audit.py | 2 +- .../dependencies/track_vulnerabilities.py | 91 ++++++++++++++----- .../track_vulnerabilities_test.py | 71 ++++++++------- 5 files changed, 123 insertions(+), 61 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 1a6832109..920f82f7a 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -15,6 +15,7 @@ To ensure usage of secure packages, it is up to the user to similarly relock the ## Features * #740: Added nox session `release:update` +* #402: Created nox task to detect resolved GitHub security issues ## Security Issues diff --git a/exasol/toolbox/nox/_dependencies.py b/exasol/toolbox/nox/_dependencies.py index dc860875a..ffa0ba902 100644 --- a/exasol/toolbox/nox/_dependencies.py +++ b/exasol/toolbox/nox/_dependencies.py @@ -9,17 +9,21 @@ from exasol.toolbox.util.dependencies.audit import ( PipAuditException, Vulnerabilities, + get_vulnerabilities, + get_vulnerabilities_from_latest_tag, ) from exasol.toolbox.util.dependencies.licenses import ( PackageLicenseReport, get_licenses, ) from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies +from exasol.toolbox.util.dependencies.track_vulnerabilities import SecurityAudit +from noxconfig import PROJECT_CONFIG @nox.session(name="dependency:licenses", python=False) def dependency_licenses(session: Session) -> None: - """Return the packages with their licenses""" + """Report licenses for all dependencies.""" dependencies = get_dependencies(working_directory=Path()) licenses = get_licenses() license_markdown = PackageLicenseReport( @@ -30,7 +34,7 @@ def dependency_licenses(session: Session) -> None: @nox.session(name="dependency:audit", python=False) def audit(session: Session) -> None: - """Check for known vulnerabilities""" + """Report known vulnerabilities.""" try: vulnerabilities = Vulnerabilities.load_from_pip_audit(working_directory=Path()) @@ -39,3 +43,14 @@ def audit(session: Session) -> None: security_issue_dict = vulnerabilities.security_issue_dict print(json.dumps(security_issue_dict, indent=2)) + + +@nox.session(name="security:audit", python=False) +def security_audit(session: Session) -> None: + """Report resolved vulnerabilities in dependencies.""" + path = PROJECT_CONFIG.root_path + audit = SecurityAudit( + previous_vulnerabilities=get_vulnerabilities_from_latest_tag(path), + current_vulnerabilities=get_vulnerabilities(path), + ) + print(audit.report_resolved_vulnerabilities()) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 53fb67352..783e21ab4 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -177,7 +177,7 @@ def audit_poetry_files(working_directory: Path) -> str: tmpdir = Path(path) (tmpdir / requirements_txt).write_text(output.stdout) - command = ["pip-audit", "-r", requirements_txt, "-f", "json"] + command = ["pip-audit", "--disable-pip", "-r", requirements_txt, "-f", "json"] output = subprocess.run( command, capture_output=True, diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index d9d663797..1afe5d7e0 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -1,3 +1,6 @@ +from inspect import cleandoc +from typing import Dict + from pydantic import ( BaseModel, ConfigDict, @@ -6,37 +9,75 @@ from exasol.toolbox.util.dependencies.audit import Vulnerability -class ResolvedVulnerabilities(BaseModel): - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - - previous_vulnerabilities: list[Vulnerability] - current_vulnerabilities: list[Vulnerability] +class VulnerabilityMatcher: + def __init__(self, current_vulnerabilities: list[Vulnerability]): + # Dict of current vulnerabilities: + # * keys: package names + # * values: set of each vulnerability's references + self._references = { + v.package.name: set(v.references) + for v in current_vulnerabilities + } - def _is_resolved(self, previous_vuln: Vulnerability): + def is_resolved(self, vuln: Vulnerability) -> bool: """ Detects if a vulnerability has been resolved. - A vulnerability is said to be resolved when it cannot be found - in the `current_vulnerabilities`. In order to see if a vulnerability - is still present, its id and aliases are compared to values in the - `current_vulnerabilities`. It is hoped that if an ID were to change - that this would still be present in the aliases. + A vulnerability is said to be resolved when it cannot be found in + the `current_vulnerabilities`. + + Vulnerabilities are matched by the name of the affected package + and the vulnerability's "references" (set of ID and aliases). + + The vulnerability is rated as "resolved" only if there is not + intersection between previous and current references. + + This hopefully compensates in case a different ID is assigned to a + vulnerability. """ - previous_vuln_set = {previous_vuln.id, *previous_vuln.aliases} - for current_vuln in self.current_vulnerabilities: - if previous_vuln.package.name == current_vuln.package.name: - current_vuln_id_set = {current_vuln.id, *current_vuln.aliases} - if previous_vuln_set.intersection(current_vuln_id_set): - return False - return True + refs = set(vuln.references) + current = self._references.get(vuln.package.name, set()) + return not refs.intersection(current) + + +class SecurityAudit(BaseModel): + """ + Compare previous vulnerabilities to current ones and create a report + about the resolved vulnerabilities. + """ + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + + previous_vulnerabilities: list[Vulnerability] + current_vulnerabilities: list[Vulnerability] @property - def resolutions(self) -> list[Vulnerability]: + def resolved(self) -> list[Vulnerability]: """ - Return resolved vulnerabilities + Return the list of resolved vulnerabilities. """ - resolved_vulnerabilities = [] - for previous_vuln in self.previous_vulnerabilities: - if self._is_resolved(previous_vuln): - resolved_vulnerabilities.append(previous_vuln) - return resolved_vulnerabilities + matcher = VulnerabilityMatcher(self.current_vulnerabilities) + return [ + vuln for vuln in self.previous_vulnerabilities + if matcher.is_resolved(vuln) + ] + + def report_resolved_vulnerabilities(self) -> str: + if not (resolved := self.resolved): + return "" + header = cleandoc( + """ + ## Fixed Vulnerabilities + + This release fixes vulnerabilities by updating dependencies: + + | Dependency | Vulnerability | Affected | Fixed in | + |------------|---------------|----------|----------| + """ + ) + def formatted(vuln: Vulnerability) -> str: + columns = (vuln.package.name, vuln.id, str(vuln.package.version), vuln.fix_versions[0]) + return f'| {" | ".join(columns)} |' + + body = "\n".join(formatted(v) for v in resolved) + return f"{header}\n{body}" diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 1dd584ac9..3c015c60e 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -1,55 +1,60 @@ +from exasol.toolbox.util.dependencies.audit import Vulnerability from exasol.toolbox.util.dependencies.track_vulnerabilities import ( - ResolvedVulnerabilities, + SecurityAudit, + VulnerabilityMatcher, ) -class TestResolvedVulnerabilities: - def test_vulnerability_present_for_previous_and_current(self, sample_vulnerability): +def _flip_id_and_alias(vulnerability: SampleVulnerability): + other = vulnerability + vuln_entry = { + "aliases": [other.vulnerability_id], + "id": other.cve_id, + "fix_versions": other.vulnerability.fix_versions, + "description": other.description, + } + return Vulnerability.from_audit_entry( + package_name=other.package_name, + version=other.version, + vuln_entry=vuln_entry, + ) + + +class TestVulnerabilityMatcher: + def test_not_resolved(self, sample_vulnerability): vuln = sample_vulnerability.vulnerability - resolved = ResolvedVulnerabilities( - previous_vulnerabilities=[vuln], current_vulnerabilities=[vuln] - ) - assert resolved._is_resolved(vuln) is False - - def test_vulnerability_present_for_previous_and_current_with_different_id( - self, sample_vulnerability - ): - vuln2 = sample_vulnerability.vulnerability.__dict__.copy() - vuln2["version"] = sample_vulnerability.version - # flipping aliases & id to ensure can match across types - vuln2["aliases"] = [sample_vulnerability.vulnerability_id] - vuln2["id"] = sample_vulnerability.cve_id - - resolved = ResolvedVulnerabilities( - previous_vulnerabilities=[sample_vulnerability.vulnerability], - current_vulnerabilities=[vuln2], - ) - assert resolved._is_resolved(sample_vulnerability.vulnerability) is False + matcher = VulnerabilityMatcher(current_vulnerabilities=[vuln]) + assert not matcher.is_resolved(vuln) + + def test_changed_id_not_resolved(self, sample_vulnerability): + vuln2 = _flip_id_and_alias(sample_vulnerability) + matcher = VulnerabilityMatcher(current_vulnerabilities=[vuln2]) + assert not matcher.is_resolved(sample_vulnerability.vulnerability) - def test_vulnerability_in_previous_resolved_in_current(self, sample_vulnerability): + def test_resolved(self, sample_vulnerability): vuln = sample_vulnerability.vulnerability - resolved = ResolvedVulnerabilities( - previous_vulnerabilities=[vuln], current_vulnerabilities=[] - ) - assert resolved._is_resolved(vuln) is True + matcher = VulnerabilityMatcher(current_vulnerabilities=[]) + assert matcher.is_resolved(vuln) + +class TestSecurityAudit: def test_no_vulnerabilities_for_previous_and_current(self): - resolved = ResolvedVulnerabilities( + audit = SecurityAudit( previous_vulnerabilities=[], current_vulnerabilities=[] ) - assert resolved.resolutions == [] + assert audit.resolved == [] def test_vulnerability_in_current_but_not_present(self, sample_vulnerability): - resolved = ResolvedVulnerabilities( + audit = SecurityAudit( previous_vulnerabilities=[], current_vulnerabilities=[sample_vulnerability.vulnerability], ) # only care about "resolved" vulnerabilities, not new ones - assert resolved.resolutions == [] + assert audit.resolved == [] def test_resolved_vulnerabilities(self, sample_vulnerability): - resolved = ResolvedVulnerabilities( + audit = SecurityAudit( previous_vulnerabilities=[sample_vulnerability.vulnerability], current_vulnerabilities=[], ) - assert resolved.resolutions == [sample_vulnerability.vulnerability] + assert audit.resolved == [sample_vulnerability.vulnerability] From 282591f569aba28690f80b5f5aa9d928ca8b87e2 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 6 Apr 2026 11:25:10 +0200 Subject: [PATCH 02/23] added typehint to get_vulnerabilities_from_latest_tag --- exasol/toolbox/util/dependencies/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 783e21ab4..836bf8382 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -239,6 +239,6 @@ def get_vulnerabilities(working_directory: Path) -> list[Vulnerability]: ).vulnerabilities -def get_vulnerabilities_from_latest_tag(root_path: Path): +def get_vulnerabilities_from_latest_tag(root_path: Path) -> list[Vulnerability]: with poetry_files_from_latest_tag(root_path=root_path) as tmp_dir: return get_vulnerabilities(working_directory=tmp_dir) From 8b1e7b8d414cf968acb4fc75b17ed09ef56acace Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 6 Apr 2026 11:25:50 +0200 Subject: [PATCH 03/23] Validated warning in test and hid warning from pytest output --- test/unit/config_test.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/unit/config_test.py b/test/unit/config_test.py index 385ab2aad..035f7a6b0 100644 --- a/test/unit/config_test.py +++ b/test/unit/config_test.py @@ -1,5 +1,6 @@ from collections.abc import Iterable from pathlib import Path +from unittest.mock import Mock import pytest from pydantic_core._pydantic_core import ValidationError @@ -9,6 +10,7 @@ BaseConfig, DependencyManager, valid_version_string, + warnings, ) from exasol.toolbox.nox.plugin import hookimpl from exasol.toolbox.util.version import Version @@ -202,9 +204,19 @@ def test_raises_exception_without_hook(test_project_config_factory): class TestDependencyManager: @staticmethod - @pytest.mark.parametrize("version", ["2.1.4", "2.3.0", "2.9.9"]) - def test_works_as_expected(version): + @pytest.mark.parametrize( + "version, expected_warning", + [ + ("2.1.4", None), + ("2.3.0", None), + ("2.9.9", "Poetry version exceeds last tested version"), + ], + ) + def test_works_as_expected(version, expected_warning, monkeypatch): + monkeypatch.setattr(warnings, "warn", Mock()) DependencyManager(name="poetry", version=version) + if expected_warning: + assert expected_warning in warnings.warn.call_args.args[0] @staticmethod def test_raises_exception_when_not_supported_name(): From 91aebce639072d7448f816dfb9ded193e7bf187e Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 6 Apr 2026 11:26:28 +0200 Subject: [PATCH 04/23] Renamed method resolved to resolved_vulnerabilities --- exasol/toolbox/util/dependencies/track_vulnerabilities.py | 4 ++-- test/unit/util/dependencies/track_vulnerabilities_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index 1afe5d7e0..0076969dc 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -52,7 +52,7 @@ class SecurityAudit(BaseModel): current_vulnerabilities: list[Vulnerability] @property - def resolved(self) -> list[Vulnerability]: + def resolved_vulnerabilities(self) -> list[Vulnerability]: """ Return the list of resolved vulnerabilities. """ @@ -63,7 +63,7 @@ def resolved(self) -> list[Vulnerability]: ] def report_resolved_vulnerabilities(self) -> str: - if not (resolved := self.resolved): + if not (resolved := self.resolved_vulnerabilities): return "" header = cleandoc( """ diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 3c015c60e..33858ca56 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -42,7 +42,7 @@ def test_no_vulnerabilities_for_previous_and_current(self): audit = SecurityAudit( previous_vulnerabilities=[], current_vulnerabilities=[] ) - assert audit.resolved == [] + assert audit.resolved_vulnerabilities == [] def test_vulnerability_in_current_but_not_present(self, sample_vulnerability): audit = SecurityAudit( @@ -50,11 +50,11 @@ def test_vulnerability_in_current_but_not_present(self, sample_vulnerability): current_vulnerabilities=[sample_vulnerability.vulnerability], ) # only care about "resolved" vulnerabilities, not new ones - assert audit.resolved == [] + assert audit.resolved_vulnerabilities == [] def test_resolved_vulnerabilities(self, sample_vulnerability): audit = SecurityAudit( previous_vulnerabilities=[sample_vulnerability.vulnerability], current_vulnerabilities=[], ) - assert audit.resolved == [sample_vulnerability.vulnerability] + assert audit.resolved_vulnerabilities == [sample_vulnerability.vulnerability] From 8d097a5c62485da485f04769787a52156f921a24 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 6 Apr 2026 11:46:24 +0200 Subject: [PATCH 05/23] Renamed nox task and class SecurityAudit once again --- exasol/toolbox/nox/_dependencies.py | 11 ++++++----- .../util/dependencies/track_vulnerabilities.py | 2 +- .../util/dependencies/track_vulnerabilities_test.py | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/exasol/toolbox/nox/_dependencies.py b/exasol/toolbox/nox/_dependencies.py index ffa0ba902..d196dfd08 100644 --- a/exasol/toolbox/nox/_dependencies.py +++ b/exasol/toolbox/nox/_dependencies.py @@ -17,7 +17,7 @@ get_licenses, ) from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies -from exasol.toolbox.util.dependencies.track_vulnerabilities import SecurityAudit +from exasol.toolbox.util.dependencies.track_vulnerabilities import DependenciesAudit from noxconfig import PROJECT_CONFIG @@ -32,7 +32,8 @@ def dependency_licenses(session: Session) -> None: print(license_markdown.to_markdown()) -@nox.session(name="dependency:audit", python=False) +# Probably this session is obsolete +@nox.session(name="dependency:audit-old", python=False) def audit(session: Session) -> None: """Report known vulnerabilities.""" @@ -45,11 +46,11 @@ def audit(session: Session) -> None: print(json.dumps(security_issue_dict, indent=2)) -@nox.session(name="security:audit", python=False) -def security_audit(session: Session) -> None: +@nox.session(name="vulnerabilities:resolved", python=False) +def report_resolved_vulnerabilities(session: Session) -> None: """Report resolved vulnerabilities in dependencies.""" path = PROJECT_CONFIG.root_path - audit = SecurityAudit( + audit = DependenciesAudit( previous_vulnerabilities=get_vulnerabilities_from_latest_tag(path), current_vulnerabilities=get_vulnerabilities(path), ) diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index 0076969dc..ec079999c 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -40,7 +40,7 @@ def is_resolved(self, vuln: Vulnerability) -> bool: return not refs.intersection(current) -class SecurityAudit(BaseModel): +class DependenciesAudit(BaseModel): """ Compare previous vulnerabilities to current ones and create a report about the resolved vulnerabilities. diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 33858ca56..cdfae905b 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -1,6 +1,6 @@ from exasol.toolbox.util.dependencies.audit import Vulnerability from exasol.toolbox.util.dependencies.track_vulnerabilities import ( - SecurityAudit, + DependenciesAudit, VulnerabilityMatcher, ) @@ -37,15 +37,15 @@ def test_resolved(self, sample_vulnerability): assert matcher.is_resolved(vuln) -class TestSecurityAudit: +class TestDependenciesAudit: def test_no_vulnerabilities_for_previous_and_current(self): - audit = SecurityAudit( + audit = DependenciesAudit( previous_vulnerabilities=[], current_vulnerabilities=[] ) assert audit.resolved_vulnerabilities == [] def test_vulnerability_in_current_but_not_present(self, sample_vulnerability): - audit = SecurityAudit( + audit = DependenciesAudit( previous_vulnerabilities=[], current_vulnerabilities=[sample_vulnerability.vulnerability], ) @@ -53,7 +53,7 @@ def test_vulnerability_in_current_but_not_present(self, sample_vulnerability): assert audit.resolved_vulnerabilities == [] def test_resolved_vulnerabilities(self, sample_vulnerability): - audit = SecurityAudit( + audit = DependenciesAudit( previous_vulnerabilities=[sample_vulnerability.vulnerability], current_vulnerabilities=[], ) From 9bd15c9c1663a31ad8f138de18288676174f92d9 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 6 Apr 2026 12:03:39 +0200 Subject: [PATCH 06/23] Added integration test --- test/unit/nox/_dependencies_test.py | 33 +++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/test/unit/nox/_dependencies_test.py b/test/unit/nox/_dependencies_test.py index b841d46d3..a391193e4 100644 --- a/test/unit/nox/_dependencies_test.py +++ b/test/unit/nox/_dependencies_test.py @@ -1,18 +1,25 @@ -from unittest import mock +from unittest.mock import Mock -from exasol.toolbox.nox._dependencies import audit +from exasol.toolbox.nox import _dependencies from exasol.toolbox.util.dependencies.audit import Vulnerabilities -class TestAudit: - @staticmethod - def test_works_as_expected_with_mock(nox_session, sample_vulnerability, capsys): - with mock.patch( - "exasol.toolbox.nox._dependencies.Vulnerabilities" - ) as mock_class: - mock_class.load_from_pip_audit.return_value = Vulnerabilities( - vulnerabilities=[sample_vulnerability.vulnerability] - ) - audit(nox_session) +# Proposal: Remove this test and the related nox task under test +def test_audit(monkeypatch, nox_session, sample_vulnerability, capsys): + monkeypatch.setattr(_dependencies, "Vulnerabilities", Mock()) + _dependencies.Vulnerabilities.load_from_pip_audit.return_value = Vulnerabilities( + vulnerabilities=[sample_vulnerability.vulnerability] + ) + _dependencies.audit(nox_session) + assert capsys.readouterr().out == sample_vulnerability.nox_dependencies_audit - assert capsys.readouterr().out == sample_vulnerability.nox_dependencies_audit + +def test_report_resolved_vulnerabilities(monkeypatch, nox_session, capsys, sample_vulnerability): + monkeypatch.setattr( + _dependencies, + "get_vulnerabilities_from_latest_tag", + Mock(return_value=[sample_vulnerability.vulnerability]), + ) + monkeypatch.setattr(_dependencies, "get_vulnerabilities", Mock(return_value=[])) + _dependencies.report_resolved_vulnerabilities(nox_session) + assert "| jinja2 | CVE-2025-27516 | 3.1.5 | 3.1.6 |" in capsys.readouterr().out From fc12b43dbd6ea9e7c0379a46cf70d9489c2ee435 Mon Sep 17 00:00:00 2001 From: ckunki Date: Thu, 9 Apr 2026 16:46:26 +0200 Subject: [PATCH 07/23] merged changes from changelog.py --- .../features/managing_dependencies.rst | 27 +++++++++++-------- exasol/toolbox/nox/_dependencies.py | 3 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/doc/user_guide/features/managing_dependencies.rst b/doc/user_guide/features/managing_dependencies.rst index 4c636e170..a89b52735 100644 --- a/doc/user_guide/features/managing_dependencies.rst +++ b/doc/user_guide/features/managing_dependencies.rst @@ -1,12 +1,17 @@ -Managing dependencies -===================== +Managing Dependencies and Vulnerabilities +========================================= -+--------------------------+------------------+----------------------------------------+ -| Nox session | CI Usage | Action | -+==========================+==================+========================================+ -| ``dependency:licenses`` | ``report.yml`` | Uses ``pip-licenses`` to return | -| | | packages with their licenses | -+--------------------------+------------------+----------------------------------------+ -| ``dependency:audit`` | No | Uses ``pip-audit`` to return active | -| | | vulnerabilities in our dependencies | -+--------------------------+------------------+----------------------------------------+ ++------------------------------+----------------+-------------------------------------+ +| Nox session | CI Usage | Action | ++==============================+================+=====================================+ +| ``dependency:licenses`` | ``report.yml`` | Uses ``pip-licenses`` to return | +| | | packages with their licenses | ++------------------------------+----------------+-------------------------------------+ +| ``dependency:audit`` | No | Uses ``pip-audit`` to report active | +| | | vulnerabilities in our dependencies | ++------------------------------+----------------+-------------------------------------+ +| ``vulnerabilities:resolved`` | No | Uses ``pip-audit`` to report known | +| | | vulnerabilities in depdendencies | +| | | that have been resolved in | +| | | comparison to the last release. | ++------------------------------+----------------+-------------------------------------+ diff --git a/exasol/toolbox/nox/_dependencies.py b/exasol/toolbox/nox/_dependencies.py index d196dfd08..c6c64401e 100644 --- a/exasol/toolbox/nox/_dependencies.py +++ b/exasol/toolbox/nox/_dependencies.py @@ -32,8 +32,7 @@ def dependency_licenses(session: Session) -> None: print(license_markdown.to_markdown()) -# Probably this session is obsolete -@nox.session(name="dependency:audit-old", python=False) +@nox.session(name="dependency:audit", python=False) def audit(session: Session) -> None: """Report known vulnerabilities.""" From f799f7ab3d138d9b8a6a0d1093940a94478032d3 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 12:14:03 +0200 Subject: [PATCH 08/23] Removed comment --- test/unit/nox/_dependencies_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/nox/_dependencies_test.py b/test/unit/nox/_dependencies_test.py index a391193e4..94b152304 100644 --- a/test/unit/nox/_dependencies_test.py +++ b/test/unit/nox/_dependencies_test.py @@ -4,7 +4,6 @@ from exasol.toolbox.util.dependencies.audit import Vulnerabilities -# Proposal: Remove this test and the related nox task under test def test_audit(monkeypatch, nox_session, sample_vulnerability, capsys): monkeypatch.setattr(_dependencies, "Vulnerabilities", Mock()) _dependencies.Vulnerabilities.load_from_pip_audit.return_value = Vulnerabilities( From 8d6f7a4c8c456b98c6761ec6bf2eadf1c707b0c4 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 12:15:59 +0200 Subject: [PATCH 09/23] Removed unused imports --- exasol/toolbox/util/dependencies/track_vulnerabilities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index ec079999c..87e05d504 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -1,5 +1,4 @@ from inspect import cleandoc -from typing import Dict from pydantic import ( BaseModel, From 99d52efa80b1c15614a80f095108a578f7e1179f Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 13:03:04 +0200 Subject: [PATCH 10/23] nox -s format:fix --- .../dependencies/track_vulnerabilities.py | 20 ++++++++++--------- test/unit/nox/_dependencies_test.py | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/exasol/toolbox/util/dependencies/track_vulnerabilities.py b/exasol/toolbox/util/dependencies/track_vulnerabilities.py index 87e05d504..db074ba3e 100644 --- a/exasol/toolbox/util/dependencies/track_vulnerabilities.py +++ b/exasol/toolbox/util/dependencies/track_vulnerabilities.py @@ -14,8 +14,7 @@ def __init__(self, current_vulnerabilities: list[Vulnerability]): # * keys: package names # * values: set of each vulnerability's references self._references = { - v.package.name: set(v.references) - for v in current_vulnerabilities + v.package.name: set(v.references) for v in current_vulnerabilities } def is_resolved(self, vuln: Vulnerability) -> bool: @@ -57,25 +56,28 @@ def resolved_vulnerabilities(self) -> list[Vulnerability]: """ matcher = VulnerabilityMatcher(self.current_vulnerabilities) return [ - vuln for vuln in self.previous_vulnerabilities - if matcher.is_resolved(vuln) + vuln for vuln in self.previous_vulnerabilities if matcher.is_resolved(vuln) ] def report_resolved_vulnerabilities(self) -> str: if not (resolved := self.resolved_vulnerabilities): return "" - header = cleandoc( - """ + header = cleandoc(""" ## Fixed Vulnerabilities This release fixes vulnerabilities by updating dependencies: | Dependency | Vulnerability | Affected | Fixed in | |------------|---------------|----------|----------| - """ - ) + """) + def formatted(vuln: Vulnerability) -> str: - columns = (vuln.package.name, vuln.id, str(vuln.package.version), vuln.fix_versions[0]) + columns = ( + vuln.package.name, + vuln.id, + str(vuln.package.version), + vuln.fix_versions[0], + ) return f'| {" | ".join(columns)} |' body = "\n".join(formatted(v) for v in resolved) diff --git a/test/unit/nox/_dependencies_test.py b/test/unit/nox/_dependencies_test.py index 94b152304..b3525a7ec 100644 --- a/test/unit/nox/_dependencies_test.py +++ b/test/unit/nox/_dependencies_test.py @@ -13,7 +13,9 @@ def test_audit(monkeypatch, nox_session, sample_vulnerability, capsys): assert capsys.readouterr().out == sample_vulnerability.nox_dependencies_audit -def test_report_resolved_vulnerabilities(monkeypatch, nox_session, capsys, sample_vulnerability): +def test_report_resolved_vulnerabilities( + monkeypatch, nox_session, capsys, sample_vulnerability +): monkeypatch.setattr( _dependencies, "get_vulnerabilities_from_latest_tag", From 540b639b3448d0774509cd6b43938266bbd1b097 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 14:27:11 +0200 Subject: [PATCH 11/23] Upload metrics.json only once and only for the main branch --- .github/workflows/ci.yml | 2 ++ .github/workflows/merge-gate.yml | 2 ++ .github/workflows/pr-merge.yml | 2 ++ .github/workflows/report.yml | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9b86cf6a..18bf86698 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: needs: - merge-gate uses: ./.github/workflows/report.yml + with: + upload-metrics: true secrets: inherit permissions: contents: read diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index 45c93c415..3c6c0ad68 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -15,6 +15,8 @@ jobs: needs: - run-fast-checks uses: ./.github/workflows/report.yml + with: + upload-metrics: false secrets: inherit permissions: contents: read diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index 482647ee8..1170c7fc7 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -27,6 +27,8 @@ jobs: needs: - run-fast-checks uses: ./.github/workflows/report.yml + with: + upload-metrics: false secrets: inherit permissions: contents: read diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 922894e0f..32a71cd6e 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -2,6 +2,11 @@ name: Status Report on: workflow_call: + inputs: + upload-metrics: + description: Whether to upload file metrics.json as artifact + type: boolean + default: false jobs: @@ -50,6 +55,7 @@ jobs: run: poetry run -- nox -s project:report -- --format json | tee metrics.json - name: Upload Artifacts + if: ${{ inputs.upload-metrics }} id: upload-artifacts uses: actions/upload-artifact@v7 with: From e8fe9a27b6146ea6e2fb5d5e590d4d10d42f824f Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 14:34:39 +0200 Subject: [PATCH 12/23] Modified trigger --- .github/workflows/ci.yml | 8 +++++++- .github/workflows/pr-merge.yml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18bf86698..14f9813f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,12 @@ on: - cron: "0 0 1/7 * *" jobs: + inspect: + runs-on: "ubuntu-24.04" + steps: + - name: Check trigger + run: echo "is scheduled? ${{ github.event_name == 'schedule' }}" + merge-gate: name: Merge Gate uses: ./.github/workflows/merge-gate.yml @@ -21,7 +27,7 @@ jobs: - merge-gate uses: ./.github/workflows/report.yml with: - upload-metrics: true + upload-metrics: ${{ github.event_name == 'schedule' }} secrets: inherit permissions: contents: read diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index 1170c7fc7..1a38c2c83 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -28,7 +28,7 @@ jobs: - run-fast-checks uses: ./.github/workflows/report.yml with: - upload-metrics: false + upload-metrics: true secrets: inherit permissions: contents: read From 0e8eced7058122ecaa1b0f8e73f30317f957dd2f Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 14:39:19 +0200 Subject: [PATCH 13/23] Modified trigger (2) --- .github/workflows/checks.yml | 8 ++++++++ .github/workflows/ci.yml | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5d9a2ab5d..ac9f99309 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -4,6 +4,14 @@ on: workflow_call: jobs: + inspect: + runs-on: "ubuntu-24.04" + steps: + - name: Check trigger + run: | + echo "is scheduled? ${{ github.event_name == 'schedule' }}" + echo "github.event_name: ${{ github.event_name }}" + check-version: name: Check Version runs-on: "ubuntu-24.04" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14f9813f1..a8a64cb0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,6 @@ on: - cron: "0 0 1/7 * *" jobs: - inspect: - runs-on: "ubuntu-24.04" - steps: - - name: Check trigger - run: echo "is scheduled? ${{ github.event_name == 'schedule' }}" - merge-gate: name: Merge Gate uses: ./.github/workflows/merge-gate.yml From b732144ef75fd5371d21008f8737623db78fbcd9 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 14:45:31 +0200 Subject: [PATCH 14/23] fixed unit test --- test/conftest.py | 22 +++++++++++++++++++ .../track_vulnerabilities_test.py | 20 ++--------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 8c7b69221..21ff2ddce 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -173,3 +173,25 @@ def sample_vulnerability() -> SampleVulnerability: @pytest.fixture(scope="session") def sample_maven_vulnerabilities() -> SampleMavenVulnerabilities: return SampleMavenVulnerabilities() + + +@pytest.fixture +def flipped_id_vulnerability(sample_vulnerability: SampleVulnerability) -> SampleVulnerability: + """ + Returns an instance of SampleVulnerability equal to + sample_vulnerability() but with ID and first alias flipped to verify + handling of vulnerabilities with changed ID. + """ + + other = sample_vulnerability + vuln_entry = { + "aliases": [other.vulnerability_id], + "id": other.cve_id, + "fix_versions": other.vulnerability.fix_versions, + "description": other.description, + } + return Vulnerability.from_audit_entry( + package_name=other.package_name, + version=other.version, + vuln_entry=vuln_entry, + ) diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index cdfae905b..f9b46df8d 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -5,30 +5,14 @@ ) -def _flip_id_and_alias(vulnerability: SampleVulnerability): - other = vulnerability - vuln_entry = { - "aliases": [other.vulnerability_id], - "id": other.cve_id, - "fix_versions": other.vulnerability.fix_versions, - "description": other.description, - } - return Vulnerability.from_audit_entry( - package_name=other.package_name, - version=other.version, - vuln_entry=vuln_entry, - ) - - class TestVulnerabilityMatcher: def test_not_resolved(self, sample_vulnerability): vuln = sample_vulnerability.vulnerability matcher = VulnerabilityMatcher(current_vulnerabilities=[vuln]) assert not matcher.is_resolved(vuln) - def test_changed_id_not_resolved(self, sample_vulnerability): - vuln2 = _flip_id_and_alias(sample_vulnerability) - matcher = VulnerabilityMatcher(current_vulnerabilities=[vuln2]) + def test_changed_id_not_resolved(self, sample_vulnerability, flipped_id_vulnerability): + matcher = VulnerabilityMatcher(current_vulnerabilities=[flipped_id_vulnerability]) assert not matcher.is_resolved(sample_vulnerability.vulnerability) def test_resolved(self, sample_vulnerability): From b14b070b8c1c158ec7e3646e8defbb2f0d6744fc Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 14:54:19 +0200 Subject: [PATCH 15/23] Updated GitHub workflows --- .github/workflows/checks.yml | 8 -------- exasol/toolbox/templates/github/workflows/cd.yml | 4 ++-- exasol/toolbox/templates/github/workflows/ci.yml | 4 +++- exasol/toolbox/templates/github/workflows/gh-pages.yml | 2 +- exasol/toolbox/templates/github/workflows/merge-gate.yml | 8 +++++--- exasol/toolbox/templates/github/workflows/pr-merge.yml | 4 +++- exasol/toolbox/templates/github/workflows/report.yml | 5 +++++ 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ac9f99309..5d9a2ab5d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -4,14 +4,6 @@ on: workflow_call: jobs: - inspect: - runs-on: "ubuntu-24.04" - steps: - - name: Check trigger - run: | - echo "is scheduled? ${{ github.event_name == 'schedule' }}" - echo "github.event_name: ${{ github.event_name }}" - check-version: name: Check Version runs-on: "ubuntu-24.04" diff --git a/exasol/toolbox/templates/github/workflows/cd.yml b/exasol/toolbox/templates/github/workflows/cd.yml index a1004456d..23176904f 100644 --- a/exasol/toolbox/templates/github/workflows/cd.yml +++ b/exasol/toolbox/templates/github/workflows/cd.yml @@ -15,7 +15,7 @@ jobs: build-and-publish: needs: - - check-release-tag + - check-release-tag name: Build & Publish uses: ./.github/workflows/build-and-publish.yml permissions: @@ -25,7 +25,7 @@ jobs: publish-docs: needs: - - build-and-publish + - build-and-publish name: Publish Documentation uses: ./.github/workflows/gh-pages.yml permissions: diff --git a/exasol/toolbox/templates/github/workflows/ci.yml b/exasol/toolbox/templates/github/workflows/ci.yml index 505f10b55..a8a64cb0b 100644 --- a/exasol/toolbox/templates/github/workflows/ci.yml +++ b/exasol/toolbox/templates/github/workflows/ci.yml @@ -18,8 +18,10 @@ jobs: report: name: Report needs: - - merge-gate + - merge-gate uses: ./.github/workflows/report.yml + with: + upload-metrics: ${{ github.event_name == 'schedule' }} secrets: inherit permissions: contents: read diff --git a/exasol/toolbox/templates/github/workflows/gh-pages.yml b/exasol/toolbox/templates/github/workflows/gh-pages.yml index de637ab1c..e0b3b856b 100644 --- a/exasol/toolbox/templates/github/workflows/gh-pages.yml +++ b/exasol/toolbox/templates/github/workflows/gh-pages.yml @@ -38,7 +38,7 @@ jobs: deploy-documentation: needs: - - build-documentation + - build-documentation permissions: contents: read pages: write diff --git a/exasol/toolbox/templates/github/workflows/merge-gate.yml b/exasol/toolbox/templates/github/workflows/merge-gate.yml index 53d128a10..b0c087a80 100644 --- a/exasol/toolbox/templates/github/workflows/merge-gate.yml +++ b/exasol/toolbox/templates/github/workflows/merge-gate.yml @@ -15,6 +15,8 @@ jobs: needs: - run-fast-checks uses: ./.github/workflows/report.yml + with: + upload-metrics: false secrets: inherit permissions: contents: read @@ -35,7 +37,7 @@ jobs: run-slow-checks: name: Slow Checks needs: - - approve-run-slow-tests + - approve-run-slow-tests uses: ./.github/workflows/slow-checks.yml secrets: inherit permissions: @@ -49,8 +51,8 @@ jobs: contents: read # If you need additional jobs to be part of the merge gate, add them below needs: - - run-fast-checks - - run-slow-checks + - run-fast-checks + - run-slow-checks # Each job requires a step, so we added this dummy step. steps: - name: Approve diff --git a/exasol/toolbox/templates/github/workflows/pr-merge.yml b/exasol/toolbox/templates/github/workflows/pr-merge.yml index 8397b92d8..1a38c2c83 100644 --- a/exasol/toolbox/templates/github/workflows/pr-merge.yml +++ b/exasol/toolbox/templates/github/workflows/pr-merge.yml @@ -25,8 +25,10 @@ jobs: report: needs: - - run-fast-checks + - run-fast-checks uses: ./.github/workflows/report.yml + with: + upload-metrics: true secrets: inherit permissions: contents: read diff --git a/exasol/toolbox/templates/github/workflows/report.yml b/exasol/toolbox/templates/github/workflows/report.yml index 12539c152..883fefab8 100644 --- a/exasol/toolbox/templates/github/workflows/report.yml +++ b/exasol/toolbox/templates/github/workflows/report.yml @@ -2,6 +2,11 @@ name: Status Report on: workflow_call: + inputs: + upload-metrics: + description: Whether to upload file metrics.json as artifact + type: boolean + default: false jobs: From 22539a9540980d8700d7e1cf7b8645ca1f367f20 Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 15:04:09 +0200 Subject: [PATCH 16/23] nox -s format:fix --- test/conftest.py | 4 +++- .../unit/util/dependencies/track_vulnerabilities_test.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 21ff2ddce..0c62ed061 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -176,7 +176,9 @@ def sample_maven_vulnerabilities() -> SampleMavenVulnerabilities: @pytest.fixture -def flipped_id_vulnerability(sample_vulnerability: SampleVulnerability) -> SampleVulnerability: +def flipped_id_vulnerability( + sample_vulnerability: SampleVulnerability, +) -> SampleVulnerability: """ Returns an instance of SampleVulnerability equal to sample_vulnerability() but with ID and first alias flipped to verify diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index f9b46df8d..4a7a04854 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -1,4 +1,3 @@ -from exasol.toolbox.util.dependencies.audit import Vulnerability from exasol.toolbox.util.dependencies.track_vulnerabilities import ( DependenciesAudit, VulnerabilityMatcher, @@ -11,8 +10,12 @@ def test_not_resolved(self, sample_vulnerability): matcher = VulnerabilityMatcher(current_vulnerabilities=[vuln]) assert not matcher.is_resolved(vuln) - def test_changed_id_not_resolved(self, sample_vulnerability, flipped_id_vulnerability): - matcher = VulnerabilityMatcher(current_vulnerabilities=[flipped_id_vulnerability]) + def test_changed_id_not_resolved( + self, sample_vulnerability, flipped_id_vulnerability + ): + matcher = VulnerabilityMatcher( + current_vulnerabilities=[flipped_id_vulnerability] + ) assert not matcher.is_resolved(sample_vulnerability.vulnerability) def test_resolved(self, sample_vulnerability): From 0de28bd306de20ff0b75f69ef25063c06992d15b Mon Sep 17 00:00:00 2001 From: ckunki Date: Mon, 13 Apr 2026 15:09:39 +0200 Subject: [PATCH 17/23] Fixed unit tests --- test/conftest.py | 24 ------------------ .../track_vulnerabilities_test.py | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 0c62ed061..8c7b69221 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -173,27 +173,3 @@ def sample_vulnerability() -> SampleVulnerability: @pytest.fixture(scope="session") def sample_maven_vulnerabilities() -> SampleMavenVulnerabilities: return SampleMavenVulnerabilities() - - -@pytest.fixture -def flipped_id_vulnerability( - sample_vulnerability: SampleVulnerability, -) -> SampleVulnerability: - """ - Returns an instance of SampleVulnerability equal to - sample_vulnerability() but with ID and first alias flipped to verify - handling of vulnerabilities with changed ID. - """ - - other = sample_vulnerability - vuln_entry = { - "aliases": [other.vulnerability_id], - "id": other.cve_id, - "fix_versions": other.vulnerability.fix_versions, - "description": other.description, - } - return Vulnerability.from_audit_entry( - package_name=other.package_name, - version=other.version, - vuln_entry=vuln_entry, - ) diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 4a7a04854..8740380fa 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -1,9 +1,34 @@ +import pytest + +from exasol.toolbox.util.dependencies.audit import Vulnerability from exasol.toolbox.util.dependencies.track_vulnerabilities import ( DependenciesAudit, VulnerabilityMatcher, ) +@pytest.fixture +def flipped_id_vulnerability(sample_vulnerability) -> Vulnerability: + """ + Returns an instance of SampleVulnerability equal to + sample_vulnerability() but with ID and first alias flipped to verify + handling of vulnerabilities with changed ID. + """ + + other = sample_vulnerability + vuln_entry = { + "aliases": [other.vulnerability_id], + "id": other.cve_id, + "fix_versions": other.vulnerability.fix_versions, + "description": other.description, + } + return Vulnerability.from_audit_entry( + package_name=other.package_name, + version=other.version, + vuln_entry=vuln_entry, + ) + + class TestVulnerabilityMatcher: def test_not_resolved(self, sample_vulnerability): vuln = sample_vulnerability.vulnerability From df330e126820c0af1d150f12bece35e1df13789c Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 14 Apr 2026 07:49:32 +0200 Subject: [PATCH 18/23] Updated workflows once again --- .github/workflows/ci.yml | 9 +++++++-- .github/workflows/merge-gate.yml | 7 +++++++ exasol/toolbox/templates/github/workflows/ci.yml | 9 +++++++-- exasol/toolbox/templates/github/workflows/merge-gate.yml | 7 +++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8a64cb0b..f58bbfe3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,17 +11,22 @@ jobs: merge-gate: name: Merge Gate uses: ./.github/workflows/merge-gate.yml + with: + root-event: ${{ github.event_name }} secrets: inherit permissions: contents: read report: + # Job merge-gate requires manual approval for running the slow checks. If + # current workflow ci.yml is triggered by schedule, there is no manual + # interaction, manual approval will never be given, slow checks will not + # be executed, merge-gate will never terminate, and the report will never + # be called. name: Report needs: - merge-gate uses: ./.github/workflows/report.yml - with: - upload-metrics: ${{ github.event_name == 'schedule' }} secrets: inherit permissions: contents: read diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index 3c6c0ad68..190d61e51 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -2,6 +2,12 @@ name: Merge-Gate on: workflow_call: + inputs: + root-event: + description: GitHub event triggering the root workflow ci.yml + required: false + type: string + default: unknown jobs: run-fast-checks: @@ -23,6 +29,7 @@ jobs: approve-run-slow-tests: name: Approve Running Slow Tests? + if: ${{ inputs.root-event != 'scheduled' }} runs-on: "ubuntu-24.04" permissions: contents: read diff --git a/exasol/toolbox/templates/github/workflows/ci.yml b/exasol/toolbox/templates/github/workflows/ci.yml index a8a64cb0b..f58bbfe3c 100644 --- a/exasol/toolbox/templates/github/workflows/ci.yml +++ b/exasol/toolbox/templates/github/workflows/ci.yml @@ -11,17 +11,22 @@ jobs: merge-gate: name: Merge Gate uses: ./.github/workflows/merge-gate.yml + with: + root-event: ${{ github.event_name }} secrets: inherit permissions: contents: read report: + # Job merge-gate requires manual approval for running the slow checks. If + # current workflow ci.yml is triggered by schedule, there is no manual + # interaction, manual approval will never be given, slow checks will not + # be executed, merge-gate will never terminate, and the report will never + # be called. name: Report needs: - merge-gate uses: ./.github/workflows/report.yml - with: - upload-metrics: ${{ github.event_name == 'schedule' }} secrets: inherit permissions: contents: read diff --git a/exasol/toolbox/templates/github/workflows/merge-gate.yml b/exasol/toolbox/templates/github/workflows/merge-gate.yml index b0c087a80..e4252e331 100644 --- a/exasol/toolbox/templates/github/workflows/merge-gate.yml +++ b/exasol/toolbox/templates/github/workflows/merge-gate.yml @@ -2,6 +2,12 @@ name: Merge-Gate on: workflow_call: + inputs: + root-event: + description: GitHub event triggering the root workflow ci.yml + required: false + type: string + default: unknown jobs: run-fast-checks: @@ -23,6 +29,7 @@ jobs: approve-run-slow-tests: name: Approve Running Slow Tests? + if: ${{ inputs.root-event != 'scheduled' }} runs-on: "(( os_version ))" permissions: contents: read From 59911f3990529251bde154f4682da6ebef64a202 Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 14 Apr 2026 07:53:34 +0200 Subject: [PATCH 19/23] fixed typo in event name --- .github/workflows/merge-gate.yml | 2 +- exasol/toolbox/templates/github/workflows/merge-gate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index 190d61e51..9e3d87547 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -29,7 +29,7 @@ jobs: approve-run-slow-tests: name: Approve Running Slow Tests? - if: ${{ inputs.root-event != 'scheduled' }} + if: ${{ inputs.root-event != 'schedule' }} runs-on: "ubuntu-24.04" permissions: contents: read diff --git a/exasol/toolbox/templates/github/workflows/merge-gate.yml b/exasol/toolbox/templates/github/workflows/merge-gate.yml index e4252e331..40cfedbee 100644 --- a/exasol/toolbox/templates/github/workflows/merge-gate.yml +++ b/exasol/toolbox/templates/github/workflows/merge-gate.yml @@ -29,7 +29,7 @@ jobs: approve-run-slow-tests: name: Approve Running Slow Tests? - if: ${{ inputs.root-event != 'scheduled' }} + if: ${{ inputs.root-event != 'schedule' }} runs-on: "(( os_version ))" permissions: contents: read From 117362cfdad8acc8943de7497f8119fb83f0f86f Mon Sep 17 00:00:00 2001 From: Christoph Kuhnke Date: Tue, 14 Apr 2026 09:58:30 +0200 Subject: [PATCH 20/23] Apply suggestions from code review Co-authored-by: Ariel Schulz <43442541+ArBridgeman@users.noreply.github.com> --- doc/changes/unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 91e34d765..b27ad4934 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -11,7 +11,7 @@ The `report.yml` is also called after the `checks.yml` completes. This allows us to get linting, security, and unit test coverage before running the `slow-checks.yml`, as described in the [Pull Request description](https://exasol.github.io/python-toolbox/main/user_guide/features/github_workflows/index.html#pull-request). -This release also adds nox session `vulnerabilities:resolved` reporting resolved GitHub security issues since the last release. +This release also adds a `vulnerabilities:resolved` Nox session, which reports GitHub security issues resolved since the last release. ## Features From ae9a41d1c302988bd3e180888e66de0cf3ca1187 Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 14 Apr 2026 10:08:01 +0200 Subject: [PATCH 21/23] Added comment for CLI option --disable-pip to pip-audit --- exasol/toolbox/util/dependencies/audit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 836bf8382..1983fc4f1 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -177,6 +177,10 @@ def audit_poetry_files(working_directory: Path) -> str: tmpdir = Path(path) (tmpdir / requirements_txt).write_text(output.stdout) + # CLI option `--disable-pip` skips dependency resolution in pip. The + # option can be used with hashed requirements files (which is the case + # here) to avoid `pip-audit` installing an isolated environment and + # speed up the audit significantly. command = ["pip-audit", "--disable-pip", "-r", requirements_txt, "-f", "json"] output = subprocess.run( command, From 05c21438a7f6a147f2cf85c8a1bfc89b5f8dab74 Mon Sep 17 00:00:00 2001 From: Christoph Kuhnke Date: Tue, 14 Apr 2026 10:09:53 +0200 Subject: [PATCH 22/23] Update .github/workflows/ci.yml Co-authored-by: Ariel Schulz <43442541+ArBridgeman@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f58bbfe3c..016be819f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: # current workflow ci.yml is triggered by schedule, there is no manual # interaction, manual approval will never be given, slow checks will not # be executed, merge-gate will never terminate, and the report will never - # be called. + # be called. If this were later changed, then we would want `report.yml` to create a `metrics.json`. name: Report needs: - merge-gate From e70a2efa7f8a436a1208a24bcc5d5864ce4f819c Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 14 Apr 2026 13:58:44 +0200 Subject: [PATCH 23/23] Fixed test naming and implementation --- .../util/dependencies/track_vulnerabilities_test.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/unit/util/dependencies/track_vulnerabilities_test.py b/test/unit/util/dependencies/track_vulnerabilities_test.py index 8740380fa..5ea3911f1 100644 --- a/test/unit/util/dependencies/track_vulnerabilities_test.py +++ b/test/unit/util/dependencies/track_vulnerabilities_test.py @@ -17,8 +17,8 @@ def flipped_id_vulnerability(sample_vulnerability) -> Vulnerability: other = sample_vulnerability vuln_entry = { - "aliases": [other.vulnerability_id], - "id": other.cve_id, + "aliases": [other.cve_id], + "id": other.vulnerability_id, "fix_versions": other.vulnerability.fix_versions, "description": other.description, } @@ -38,6 +38,14 @@ def test_not_resolved(self, sample_vulnerability): def test_changed_id_not_resolved( self, sample_vulnerability, flipped_id_vulnerability ): + """ + Simulate a vulnerability to be still present, but it's ID having + changed over time. + + The test verifies that the vulnerability (using the original ID) is + still matched as "not resolved". + """ + matcher = VulnerabilityMatcher( current_vulnerabilities=[flipped_id_vulnerability] )