diff --git a/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md new file mode 100644 index 0000000000..4a19c4bb5e --- /dev/null +++ b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md @@ -0,0 +1,8 @@ +### Noema document reader transitive security updates + +- Updated the locked `fast-uri` dependency from 3.1.7 to 3.1.8 and + `ip-address` from 10.7.0 to 10.7.1. These are the first releases outside + the affected ranges for GHSA-hrr3-gc8f-f4qj, GHSA-j6r3-76f7-8jcv, and + GHSA-h3mg-xc3c-68pw. The direct dependency ranges are unchanged. +- Added a deterministic regression contract that rejects reintroduction of + vulnerable hoisted or nested copies of either transitive package. diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json index 1026fd79a3..fe692cdf0e 100644 --- a/scripts/ci/noema-document-reader/package-lock.json +++ b/scripts/ci/noema-document-reader/package-lock.json @@ -458,9 +458,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -669,9 +669,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.1.tgz", + "integrity": "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/scripts/ci/noema-document-reader/package.json b/scripts/ci/noema-document-reader/package.json index aa4fc0d3ff..2408b5854b 100644 --- a/scripts/ci/noema-document-reader/package.json +++ b/scripts/ci/noema-document-reader/package.json @@ -5,5 +5,9 @@ "dependencies": { "@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0" + }, + "overrides": { + "fast-uri": "3.1.8", + "ip-address": "10.7.1" } } diff --git a/tests/test_noema_document_reader_dependency_security.py b/tests/test_noema_document_reader_dependency_security.py new file mode 100644 index 0000000000..77c28ae30c --- /dev/null +++ b/tests/test_noema_document_reader_dependency_security.py @@ -0,0 +1,63 @@ +"""Keep the Noema document reader above known vulnerable transitive releases.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +LOCK_FILE = ( + Path(__file__).resolve().parents[1] + / "scripts" + / "ci" + / "noema-document-reader" + / "package-lock.json" +) +PACKAGE_FILE = LOCK_FILE.with_name("package.json") + + +def _locked_versions(lock_data: dict[str, object], package_name: str) -> list[tuple[int, ...]]: + """Return every hoisted or nested locked release for one package.""" + package_records = lock_data["packages"] + assert isinstance(package_records, dict) + path_suffix = f"node_modules/{package_name}" + release_versions: list[tuple[int, ...]] = [] + for package_path, package_data in package_records.items(): + if package_path != path_suffix and not package_path.endswith(f"/{path_suffix}"): + continue + assert isinstance(package_data, dict) + release_versions.append(tuple(int(part) for part in package_data["version"].split("."))) + return release_versions + + +def test_document_reader_transitives_include_security_fixes() -> None: + """Reject releases affected by the September 2026 URI and IP advisories.""" + lock_data = json.loads(LOCK_FILE.read_text(encoding="utf-8")) + for package_name, minimum_version in { + "fast-uri": (3, 1, 8), + "ip-address": (10, 7, 1), + }.items(): + locked_versions = _locked_versions(lock_data, package_name) + assert locked_versions + assert all(version >= minimum_version for version in locked_versions) + + +def test_nested_vulnerable_transitive_is_detected() -> None: + """Do not let a patched hoisted package hide a vulnerable nested copy.""" + lock_data = { + "packages": { + "node_modules/ip-address": {"version": "10.7.1"}, + "node_modules/parent/node_modules/ip-address": {"version": "10.7.0"}, + } + } + assert _locked_versions(lock_data, "ip-address") == [(10, 7, 1), (10, 7, 0)] + + +def test_document_reader_source_owns_transitive_security_fixes() -> None: + """Require source overrides so lock regeneration preserves the repair.""" + package_data = json.loads(PACKAGE_FILE.read_text(encoding="utf-8")) + + assert package_data["overrides"] == { + "fast-uri": "3.1.8", + "ip-address": "10.7.1", + }