QUELLCODE / PY

test_privacy_check.py

project-site/test_privacy_check.py

1# OpenAss: nur privat und experimentell. Bau, Download und Nutzung auf eigene Verantwortung.2# Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder sicherheitskritische Systeme anschließen oder bedienen.3# Keine freiwillige Funktions-/Sicherheitsgarantie; zwingende gesetzliche Ansprüche bleiben unberührt. Siehe DISCLAIMER.txt.4"""Regression tests for the public release privacy guard using synthetic fixtures."""5from __future__ import annotations67import gzip8import io9import struct10import unittest11import zipfile12import zlib13from pathlib import Path14from unittest.mock import patch1516import privacy_check as privacy171819def png_chunk(kind: bytes, payload: bytes) -> bytes:20    content = kind + payload21    return struct.pack('>I', len(payload)) + content + struct.pack('>I', zlib.crc32(content) & 0xffffffff)222324def png_bytes(extra_kind: bytes | None = None, extra_payload: bytes = b'') -> bytes:25    header = png_chunk(b'IHDR', struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0))26    extra = png_chunk(extra_kind, extra_payload) if extra_kind is not None else b''27    pixels = png_chunk(b'IDAT', zlib.compress(b'\x00\x30\x40\x50'))28    return privacy.PNG_MAGIC + header + extra + pixels + png_chunk(b'IEND', b'')293031def zip_bytes(member_name: str, payload: bytes) -> bytes:32    output = io.BytesIO()33    with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED) as archive:34        archive.writestr(member_name, payload)35    return output.getvalue()363738class PrivacyGuardTests(unittest.TestCase):39    def setUp(self) -> None:40        privacy._checked.clear()4142    def assert_blocked(self, category: str, payload: bytes, label: str = 'fixture.txt', **kwargs: int) -> None:43        with self.assertRaises(ValueError) as caught:44            privacy.assert_public_bytes(label, payload, **kwargs)45        self.assertEqual(str(caught.exception), f'Public export blocked: {label} [{category}]')4647    def test_personal_home_paths_are_blocked(self) -> None:48        fixtures = [49            b'/' + b'Users' + b'/sample-account/project/file.py',50            b'/' + b'home' + b'/sample-account/project/file.py',51        ]52        for payload in fixtures:53            with self.subTest(platform='POSIX', length=len(payload)):54                self.assert_blocked('personal home-directory path', payload)55        windows = b'C:' + b'\\' + b'Users' + b'\\sample-account\\project'56        self.assert_blocked('personal Windows path', windows)5758    def test_current_account_identifier_is_blocked_without_a_path(self) -> None:59        account = 'sample' + '-account'60        synthetic_home = Path('/' + 'Users' + '/' + account)61        with patch.object(privacy.Path, 'home', return_value=synthetic_home):62            self.assert_blocked('local user identifier', b'author=' + account.upper().encode())6364    def test_provider_credentials_are_blocked_without_printing_values(self) -> None:65        examples = {66            'OpenAI key': b'sk-' + b'A' * 48,67            'GitHub token': b'gh' + b'p_' + b'A' * 40,68            'GitLab token': b'gl' + b'pat-' + b'A' * 24,69            'AWS key': b'AK' + b'IA' + b'A' * 16,70            'Google key': b'AI' + b'za' + b'A' * 35,71            'Slack token': b'xox' + b'b-' + b'A' * 24,72            'credential in URL': b'https:' + b'//operator:synthetic-password@example.test/',73        }74        for category, payload in examples.items():75            with self.subTest(category=category):76                self.assert_blocked(category, payload)7778    def test_private_key_block_is_blocked(self) -> None:79        block = b'-----BEGIN ' + b'OPENSSH ' + b'PRIVATE KEY-----\n' + b'A' * 4880        self.assert_blocked('private key', block)8182    def test_nested_zip_contents_are_checked(self) -> None:83        token = b'sk-' + b'A' * 4884        inner = zip_bytes('fixture.txt', token)85        outer = zip_bytes('inner.zip', inner)86        label = 'fixture.zip!inner.zip!fixture.txt'87        with self.assertRaises(ValueError) as caught:88            privacy.assert_public_bytes('fixture.zip', outer)89        self.assertEqual(str(caught.exception), f'Public export blocked: {label} [OpenAI key]')9091    def test_excessively_nested_archives_are_blocked(self) -> None:92        payload = b'harmless fixture'93        for _ in range(6):94            payload = zip_bytes('inner.zip', payload)95        with self.assertRaises(ValueError) as caught:96            privacy.assert_public_bytes('fixture.zip', payload)97        self.assertTrue(str(caught.exception).endswith('[excessively nested archive]'))9899    def test_gzip_blender_payload_is_checked(self) -> None:100        token = b'sk-' + b'A' * 48101        self.assert_blocked('OpenAI key', gzip.compress(b'BLENDER-v300\x00' + token), 'fixture.blend')102        privacy.assert_public_bytes('clean.blend', gzip.compress(b'BLENDER-v300' + b'clean fixture'))103104    def test_zstd_blender_payload_is_checked(self) -> None:105        try:106            from compression import zstd107            compress = zstd.compress108        except ImportError:109            try:110                import zstandard111            except ImportError:112                self.skipTest('Python 3.14 compression.zstd or zstandard is required')113            compress = zstandard.ZstdCompressor().compress114        token = b'sk-' + b'A' * 48115        self.assert_blocked('OpenAI key', compress(b'BLENDER-v300\x00' + token), 'fixture.blend')116        privacy.assert_public_bytes('clean.blend', compress(b'BLENDER-v300' + b'clean fixture'))117118    def test_blender_format_cannot_be_bypassed_by_content_cache(self) -> None:119        payload = b'ordinary text fixture'120        privacy.assert_public_bytes('fixture.txt', payload)121        self.assert_blocked('unrecognized Blender format', payload, 'fixture.blend')122123    def test_depth_limit_cannot_be_bypassed_by_content_cache(self) -> None:124        payload = b'ordinary text fixture'125        privacy.assert_public_bytes('fixture.txt', payload)126        self.assert_blocked('excessively nested archive', payload, depth=5)127128    def test_png_text_and_exif_metadata_are_blocked(self) -> None:129        fixtures = {130            b'tEXt': b'Comment\x00synthetic metadata',131            b'zTXt': b'Comment\x00\x00' + zlib.compress(b'synthetic metadata'),132            b'iTXt': b'Comment\x00\x00\x00\x00\x00synthetic metadata',133            b'eXIf': b'II\x2a\x00synthetic metadata',134        }135        for kind, content in fixtures.items():136            with self.subTest(chunk=kind.decode()):137                self.assert_blocked('PNG text or EXIF metadata', png_bytes(kind, content), 'fixture.png')138139    def test_png_crc_is_validated(self) -> None:140        payload = bytearray(png_bytes())141        payload[-1] ^= 1142        self.assert_blocked('invalid PNG checksum', bytes(payload), 'fixture.png')143144    def test_png_chunk_length_is_validated(self) -> None:145        truncated = png_bytes()[:-5]146        self.assert_blocked('invalid PNG chunk', truncated, 'fixture.png')147148    def test_clean_images_generic_pi_path_and_empty_env_pass(self) -> None:149        examples = [150            ('fixture.png', png_bytes()),151            ('setup.txt', b'/home/pi/hardware-box'),152            ('env.example', b'OPENAI_API_KEY=\nBOX_WEB_TOKEN=\n'),153            ('fixture.zip', zip_bytes('README.txt', b'clean reference fixture')),154        ]155        for label, payload in examples:156            with self.subTest(label=label):157                privacy.assert_public_bytes(label, payload)158159160if __name__ == '__main__':161    unittest.main()