From 0086cafc8768ce8f90302930fc0cbb74749c3ac2 Mon Sep 17 00:00:00 2001 From: Public Source Publisher Date: Wed, 14 Oct 2026 08:53:52 +0000 Subject: [PATCH] Publish curated ITWorx_MCP_Hub source from ae713b53e32933192b7446ac25fd6394e69eb133 --- PUBLIC_SOURCE_EXPORT.md | 2 +- PUBLIC_SOURCE_MANIFEST.json | 11 +- tests/test_mcp_hub_release_images.py | 2 + tests/test_scanner_private_files.py | 156 +++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 tests/test_scanner_private_files.py diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index 7904ad4..dc160b7 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `535009426b1f754478673ad9789b16e2c047a32e`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `ae713b53e32933192b7446ac25fd6394e69eb133`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 3990f25..9b0c653 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "535009426b1f754478673ad9789b16e2c047a32e", + "sourceRevision": "ae713b53e32933192b7446ac25fd6394e69eb133", "files": [ { "path": ".dockerignore", @@ -3379,8 +3379,8 @@ }, { "path": "tests/test_mcp_hub_release_images.py", - "sha256": "38cefc6b3d12f8c197ae522c5bd6ecc9e20407bb3be2dca7fe593a87229eecca", - "bytes": 31898 + "sha256": "d485996a59dcadd9b974245aaee8f0ab3caee75c535d341422de6b213eee92e0", + "bytes": 32010 }, { "path": "tests/test_mcp_workflow_prompts.py", @@ -3462,6 +3462,11 @@ "sha256": "3a3a6674c9e3146827cc34aa493e0445505067b6e90d5618c3552c7dc2861118", "bytes": 3902 }, + { + "path": "tests/test_scanner_private_files.py", + "sha256": "772b87fafb5a877a32234ab10b6ed418445575ac9dc0e13380491d294c55330c", + "bytes": 5048 + }, { "path": "tests/test_stitch_connector.py", "sha256": "c34cd2dc20721052dc63a15dc763ecc9b4f1adaef1d6ec2d5189adff9161c269", diff --git a/tests/test_mcp_hub_release_images.py b/tests/test_mcp_hub_release_images.py index 499fda9..65c980d 100644 --- a/tests/test_mcp_hub_release_images.py +++ b/tests/test_mcp_hub_release_images.py @@ -707,6 +707,7 @@ def test_scanner_stdout_bound_does_not_cap_or_capture_cache_files(tmp_path: Path def fake_scanner(command: list[str], **kwargs: object) -> subprocess.Popen[bytes]: assert "preexec_fn" not in kwargs assert kwargs.get("stdout") == subprocess.PIPE + assert kwargs.get("umask") == 0o077 code = ( "from pathlib import Path; import os; " "Path('cache.bin').write_bytes(b'x' * (3 * 1024 * 1024)); " @@ -728,6 +729,7 @@ def test_scanner_stdout_bound_does_not_cap_or_capture_cache_files(tmp_path: Path assert output == b'{"complete":true}' assert (tmp_path / "cache.bin").stat().st_size == 3 * 1024 * 1024 + assert (tmp_path / "cache.bin").stat().st_mode & 0o777 == 0o600 @pytest.mark.skipif( diff --git a/tests/test_scanner_private_files.py b/tests/test_scanner_private_files.py new file mode 100644 index 0000000..726f2c4 --- /dev/null +++ b/tests/test_scanner_private_files.py @@ -0,0 +1,156 @@ +import os +import stat +import subprocess +import sys +from types import SimpleNamespace + +import pytest + +from scripts import build_mcp_hub_release_images as producer + + +def test_database_download_child_has_private_umask_without_changing_other_stages(tmp_path): + calls = [] + + def runner(command, **kwargs): + calls.append(kwargs) + return subprocess.CompletedProcess(command, 0) + + for stage in ("trivy_db_download", "compose_local_build"): + producer._execute(["fixture"], env={}, cwd=tmp_path, timeout=1, stage=stage, runner=runner) + assert calls[0]["umask"] == 0o077 + assert "umask" not in calls[1] + + +@pytest.mark.parametrize( + "fault", + [ + None, + "owner", + "symlink", + "hardlink", + "oversize", + "parent", + "db_link", + "swap", + "parent_swap", + "after_swap", + ], +) +def test_db_metadata_adapter_restricts_only_owned_anchored_inode(monkeypatch, fault): + def info(inode, mode, **extra): + fields = dict( + st_dev=1, st_ino=inode, st_mode=mode, st_uid=123, st_nlink=1, st_size=16, st_mtime_ns=1 + ) + fields.update(extra) + return SimpleNamespace(**fields) + + parent = info(1, stat.S_IFDIR | (0o755 if fault == "parent" else 0o700)) + db = info(2, stat.S_IFDIR | 0o755) + metadata = info(3, stat.S_IFREG | 0o644) + if fault == "owner": + metadata.st_uid = 999 + if fault == "symlink": + metadata.st_mode = stat.S_IFLNK | 0o644 + if fault == "hardlink": + metadata.st_nlink = 2 + if fault == "oversize": + metadata.st_size = 65537 + restricted = [] + closed = [] + opens = [] + successful = [] + cache = SimpleNamespace(lstat=lambda: parent) + + def open_file(path, flags, *, dir_fd=None): + opens.append((path, dir_fd)) + assert flags & 0x20000 + if path is cache: + successful.append(10) + return 10 + if path == "db": + assert dir_fd == 10 + if fault == "db_link": + raise OSError("fixture-private-value") + successful.append(11) + return 11 + assert path == "metadata.json" and dir_fd == 11 + successful.append(12) + return 12 + + def fstat(fd): + if fd == 10: + return parent + if fd == 11: + return db + if fault == "swap": + return info(4, metadata.st_mode) + return SimpleNamespace(**vars(metadata)) + + def inspect(path, *, dir_fd, follow_symlinks): + assert follow_symlinks is False + if path == "db": + assert dir_fd == 10 + return info(4, db.st_mode) if fault == "parent_swap" else db + assert path == "metadata.json" and dir_fd == 11 + return info(4, metadata.st_mode) if fault == "after_swap" and restricted else metadata + + def restrict(fd, mode): + assert fd == 12 and mode == 0o600 + restricted.append((fd, mode)) + metadata.st_mode = stat.S_IFREG | 0o600 + + monkeypatch.setattr(producer.platform, "system", lambda: "Linux") + monkeypatch.setattr(producer, "_effective_uid", lambda: 123) + monkeypatch.setattr( + producer, + "os", + SimpleNamespace( + O_RDONLY=os.O_RDONLY, + O_DIRECTORY=0x10000, + O_NOFOLLOW=0x20000, + open=open_file, + fstat=fstat, + stat=inspect, + fchmod=restrict, + close=closed.append, + ), + ) + if fault: + with pytest.raises(producer.BuildRejected) as error: + producer._private_db_metadata(cache) + assert "fixture-private-value" not in str(error.value) + if fault != "after_swap": + assert restricted == [] + else: + producer._private_db_metadata(cache) + assert restricted == [(12, 0o600)] + assert closed == list(reversed(successful)) + + +@pytest.mark.skipif(os.name != "posix", reason="file-descriptor permission adapter requires Linux") +def test_explicit_upstream_chmod_is_restricted_before_strict_json_reader(tmp_path): + cache = tmp_path / "cache" + db = cache / "db" + db.mkdir(parents=True) + cache.chmod(0o700) + db.chmod(0o755) + metadata = db / "metadata.json" + metadata.write_text('{"Version":2}') + metadata.chmod(0o644) + with pytest.raises(producer.BuildRejected, match="not private and bounded"): + producer._json_file(metadata, maximum=64 * 1024) + producer._private_db_metadata(cache) + assert metadata.stat().st_mode & 0o777 == 0o600 + assert producer._json_file(metadata, maximum=64 * 1024) == {"Version": 2} + + +@pytest.mark.skipif(os.name != "posix", reason="kernel umask requires Linux") +def test_kernel_download_child_creates_private_files(tmp_path): + command = [ + sys.executable, + "-c", + "from pathlib import Path; Path('metadata.json').write_text('{}')", + ] + producer._execute(command, env=os.environ, cwd=tmp_path, timeout=10, stage="trivy_db_download") + assert (tmp_path / "metadata.json").stat().st_mode & 0o777 == 0o600