diff --git a/CHANGELOG.md b/CHANGELOG.md index 852778c..6b5363d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,10 @@ under **Unreleased** until a new version is selected and published. ### Fixed +- Preserved fail-closed distribution provenance across version parsing, + brandless FE/BE component fallback, and certification evidence validation, + preventing unknown or mismatched brands from inheriting Apache Doris + certification. - Updated the locked Marshmallow and Virtualenv transitive dependencies to releases that address CVE-2025-68480 and symlink-based TOCTOU vulnerabilities in development-environment creation. diff --git a/doris_mcp_server/tools/capability_detector.py b/doris_mcp_server/tools/capability_detector.py index 88c35be..765b6a3 100644 --- a/doris_mcp_server/tools/capability_detector.py +++ b/doris_mcp_server/tools/capability_detector.py @@ -2450,7 +2450,9 @@ def _component_version( # Brandless component builds (for example "4.0.6" or "4.0.6-abc1234") # carry no brand of their own; inherit the cluster brand observed in # @@version_comment so distribution provenance is not lost. - brand = default_brand or "doris" + if default_brand is None: + return parsed + brand = default_brand return parse_doris_version_comment(f"{brand} version {brand}-{raw}") diff --git a/doris_mcp_server/tools/doris_feature_matrix.py b/doris_mcp_server/tools/doris_feature_matrix.py index 9e93ab4..b19659d 100644 --- a/doris_mcp_server/tools/doris_feature_matrix.py +++ b/doris_mcp_server/tools/doris_feature_matrix.py @@ -462,29 +462,19 @@ def _validate_evidence(self) -> Self: *self.backend_version_comments, ) for comment in comments: - if self.brand == "doris": - observed = parse_doris_version_comment(comment) - if ( - not observed.is_parsed - or observed.core != target_literal - ): - raise ValueError( - "every certified FE and BE must report the Doris core " - f"version {target_literal}" - ) - else: - # Distribution evidence is validated without relying on the - # runtime brand registry: the comment must carry the declared - # brand token and the certified three-part core version. - if not _distribution_comment_matches( - comment, - brand=self.brand, - core=target_literal, - ): - raise ValueError( - f"every certified FE and BE must report the {self.brand} " - f"core version {target_literal}" - ) + # Evidence validation is independent of the mutable runtime brand + # registry. Every comment must carry the exact declared brand and + # three-part core version before it can certify that distribution. + if not _certification_comment_matches( + comment, + brand=self.brand, + core=target_literal, + ): + display_brand = "Doris" if self.brand == "doris" else self.brand + raise ValueError( + f"every certified FE and BE must report the {display_brand} " + f"core version {target_literal}" + ) expected_cases = { ("stdio", "hierarchical"), @@ -990,7 +980,7 @@ def _ordered_unique(values: Iterable[object]) -> tuple[str, ...]: return tuple(dict.fromkeys(str(value) for value in values)) -def _distribution_comment_matches( +def _certification_comment_matches( comment: str, *, brand: str, @@ -998,11 +988,16 @@ def _distribution_comment_matches( ) -> bool: """Match one evidence brand and core without the mutable alias registry.""" escaped_brand = re.escape(brand) + leading_brand = ( + rf"(?:apache\s+{escaped_brand}|{escaped_brand})" + if brand == "doris" + else escaped_brand + ) escaped_core = re.escape(core) pattern = re.compile( rf""" (? re.Pattern[str]: - brand_alternatives = "|".join((r"apache\s+doris", *brands)) - prefix_alternatives = "|".join(brands) + brand_alternatives = "|".join( + (r"apache\s+doris", *(re.escape(brand) for brand in brands)) + ) + prefix_alternatives = "|".join(re.escape(brand) for brand in brands) return re.compile( rf""" (?{brand_alternatives}) (?:\s*,?\s*version)? - (?:\s+(?:{prefix_alternatives})-|\s*-\s*|\s+) + (?:\s+(?P{prefix_alternatives})-|\s*-\s*|\s+) (?P\d+\.\d+\.\d+) (?:-(?Prc\d+|alpha\d*|beta\d*))? (?:-(?P[0-9a-f]{{7,40}}))? @@ -161,6 +163,11 @@ def parse_doris_version_comment(comment: str) -> DorisVersion: if match is None: return DorisVersion(raw=comment, deployment_hint=deployment_hint) + brand = _normalize_brand(match.group("brand")) + version_brand = match.group("version_brand") + if version_brand is not None and _normalize_brand(version_brand) != brand: + return DorisVersion(raw=comment, deployment_hint=deployment_hint) + major, minor, patch = (int(part) for part in match.group("core").split(".")) prerelease = match.group("prerelease") commit = match.group("commit") @@ -173,7 +180,7 @@ def parse_doris_version_comment(comment: str) -> DorisVersion: prerelease=prerelease.lower() if prerelease else None, commit=commit.lower() if commit else None, deployment_hint=deployment_hint, - brand=_normalize_brand(match.group("brand")), + brand=brand, parse_status=DorisVersionParseStatus.PARSED, ) diff --git a/test/tools/test_capability_detector.py b/test/tools/test_capability_detector.py index f6f9acd..1922c73 100644 --- a/test/tools/test_capability_detector.py +++ b/test/tools/test_capability_detector.py @@ -1127,6 +1127,53 @@ async def test_detector_propagates_comment_brand_to_brandless_components( assert report.certified is False +@pytest.mark.asyncio +async def test_unknown_brand_keeps_brandless_components_unknown() -> None: + connection = _ProbeConnection() + connection.row_overrides["SELECT @@version_comment;"] = [ + {"@@version_comment": "unregistereddb version 4.0.5"} + ] + connection.row_overrides["SHOW FRONTENDS"] = [ + { + "Name": "fe-1", + "IsMaster": "true", + "Version": "4.0.5", + }, + ] + connection.row_overrides["SHOW BACKENDS"] = [ + { + "BackendId": "1", + "Version": "4.0.5", + }, + ] + detector = DorisCapabilityDetector( # type: ignore[arg-type] + _ProbeConnectionManager(connection) + ) + + snapshot = await detector.detect_base( + None, + capability_generation=1, + provider_generation="provider.a", + ) + feature = DORIS_FEATURE_MATRIX.evaluate( + domain="doris_catalog", + child_name="list_tables", + versions=snapshot.version_vector, + ) + report = DORIS_PATCH_CERTIFICATION_MATRIX.evaluate(snapshot.version_vector) + + assert snapshot.version_vector.master_fe.is_parsed is False + assert snapshot.version_vector.master_fe.brand is None + assert snapshot.version_vector.backends[0].is_parsed is False + assert snapshot.version_vector.backends[0].brand is None + assert feature.compatible is False + assert feature.reason_code == "DORIS_VERSION_UNKNOWN" + assert feature.certified is False + assert report.status is VersionCertificationStatus.UNKNOWN + assert report.certified is False + assert report.evidence_ids == () + + @pytest.mark.asyncio async def test_detector_does_not_treat_commit_substrings_as_brand_tokens() -> None: configure_version_brands(("db",)) diff --git a/test/tools/test_doris_feature_matrix.py b/test/tools/test_doris_feature_matrix.py index 1f0651c..827cb2e 100644 --- a/test/tools/test_doris_feature_matrix.py +++ b/test/tools/test_doris_feature_matrix.py @@ -567,6 +567,25 @@ def test_distribution_evidence_validates_comments_without_brand_registry() -> No PatchCertificationEvidence.model_validate(payload) +def test_apache_evidence_rejects_distribution_comments_with_registered_alias( + enterprise_brand_alias: str, +) -> None: + evidence = _certification_evidence("4.0.5") + payload = evidence.model_dump(mode="python") + payload["master_fe_version_comment"] = ( + f"{enterprise_brand_alias} version 4.0.5" + ) + payload["backend_version_comments"] = ( + f"{enterprise_brand_alias}-4.0.5-abc1234", + ) + + with pytest.raises( + ValidationError, + match="Doris core version 4.0.5", + ): + PatchCertificationEvidence.model_validate(payload) + + def test_patch_evidence_rejects_incomplete_host_or_component_proof() -> None: evidence = _certification_evidence() payload = evidence.model_dump(mode="python") diff --git a/test/tools/test_doris_version.py b/test/tools/test_doris_version.py index 41a83d5..8524e9c 100644 --- a/test/tools/test_doris_version.py +++ b/test/tools/test_doris_version.py @@ -130,6 +130,25 @@ def test_registered_brand_alias_comments_parse_three_part_version( assert version.brand_verified is False +@pytest.mark.parametrize( + "comment", + [ + "Doris version enterprisedb-4.0.5", + "enterprisedb version doris-4.0.5", + ], +) +def test_mismatched_leading_and_version_brands_fail_closed( + comment: str, + enterprise_brand_alias: str, +) -> None: + version = parse_doris_version_comment(comment) + + assert version.parse_status is DorisVersionParseStatus.UNKNOWN + assert version.is_parsed is False + assert version.brand is None + assert version.core is None + + def test_brand_alias_configuration_replaces_previous_set( enterprise_brand_alias: str, ) -> None: