diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c61c142b..3a47d0a525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Validate final documentation PNG bytes through chunk, CRC, zlib-stream, + palette, dimension, and scanline contracts before granting the narrow + Pingora-policy documentation exception. - Harden the review sidecar's per-account catalog cap against silent drift: `contextual_orchestrator_review_launcher.py`'s two `build_zdr_prioritized_catalog` call sites now source their diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 805e538b86..78eeb78728 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -27,8 +27,10 @@ so a governed shared implementation is required. contracts. Environment deployment remains in `linux-cluster-ops`. 4. The organization required workflow rejects active Nginx runtime artifacts in changed final files without executing pull-request code. -5. Only dedicated source fixtures and the policy scanner may contain denied Nginx - samples; executable integration and end-to-end test helpers remain candidates +5. Documentation prose, license text, dedicated source fixtures, the policy + scanner, and structurally validated PNG evidence beneath documentation directories + may contain denied Nginx samples. Image extensions alone do not establish the + exception; executable integration and end-to-end test helpers remain candidates for enforcement. 6. Initial migration does not use Pingora's experimental cache integration. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter @@ -66,5 +68,6 @@ so a governed shared implementation is required. The policy scanner has 100% production statement and branch coverage, bounded GitHub API evidence, path/control escaping, pagination limits, exact-head content -inspection, and fail-closed malformed-evidence tests. Product migrations require -site/proxy behavior tests and deployment-specific smoke tests before cutover. +inspection, raster-format validation, and fail-closed malformed-evidence tests. +Product migrations require site/proxy behavior tests and deployment-specific +smoke tests before cutover. diff --git a/docs/doctoring/pingora-edge-standard.md b/docs/doctoring/pingora-edge-standard.md index bb624a9c56..d341662f9c 100644 --- a/docs/doctoring/pingora-edge-standard.md +++ b/docs/doctoring/pingora-edge-standard.md @@ -20,8 +20,10 @@ dependencies. The initial CWL implementation avoids experimental cache APIs. - The shared artifact is Apache-2.0 compatible with CWL permissive-license policy. - Required-workflow code is bound to its immutable central SHA and never executes pull-request content. -- Runtime evidence is bounded to one-megabyte UTF-8 regular files and a maximum of - 3,000 changed files; missing or malformed evidence fails closed. +- Runtime evidence is bounded to one-megabyte regular-file bytes, with UTF-8 + decoding for runtime candidates and complete PNG chunk/CRC/zlib/scanline + validation for documentation images; the maximum is 3,000 changed files and missing or + malformed evidence fails closed. - Exact-head product tests cover host/path routing, SPA fallback, security headers, WebSocket/streaming, body limits, health, metrics, TLS, and graceful shutdown as applicable. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 4d4c0752e1..0b976be046 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -6,12 +6,15 @@ ContextualWisdomLab production and test edge runtimes use **Cloudflare Pingora** Active Nginx containers, packages, commands, configuration files, Kubernetes Nginx ingress annotations/classes, and host-service units are prohibited. -This is a runtime boundary, not a vocabulary ban. Documentation, license notices, +This is a runtime boundary, not a vocabulary ban. Documentation, recognized +non-executable image evidence beneath a documentation directory, license notices, dedicated source fixtures under `tests/fixtures/`, the scanner source itself, and -migration histories may name Nginx. Executable integration and end-to-end test -helpers remain runtime candidates. Pull requests that modify a runtime candidate -are evaluated against the final exact head file, so deleting a legacy artifact is -allowed while preserving it or introducing a new one fails closed. +migration histories may name Nginx. An image suffix alone is not an exception: +final bounded bytes must be a structurally valid, completely decodable PNG. Executable integration +and end-to-end test helpers remain runtime candidates. Pull requests that modify a +runtime candidate are evaluated against the final exact head file, so deleting a +legacy artifact is allowed while preserving it or introducing a new one fails +closed. ## Why this is not a search-and-replace @@ -51,10 +54,12 @@ route/site contracts. Product repositories do not fork proxy internals. The organization-required `required-workflow-bootstrap` job runs trusted base-branch scanner code at the immutable required-workflow SHA. It reads bounded -changed-file metadata and final UTF-8 content through GitHub's REST API. It does -not check out or execute pull-request content and receives only read permissions. -Malformed, truncated, binary, symlink, oversized, or unavailable evidence fails -closed. +changed-file metadata and final file bytes through GitHub's REST API; runtime +candidates must decode as UTF-8, while documentation PNG evidence must pass +chunk-order, CRC, zlib-stream, dimension, and scanline validation and match +its supported format. It does not check out or execute pull-request content and +receives only read permissions. Malformed, truncated, unrecognized binary, +symlink, oversized, or unavailable evidence fails closed. ## Exception process diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 06694fcbca..b3387dfb8e 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -15,6 +15,7 @@ import os import re import sys +import zlib from dataclasses import dataclass from pathlib import PurePosixPath from typing import Callable, Mapping, Sequence @@ -24,6 +25,7 @@ MAX_FILE_BYTES = 1_048_576 MAX_RESPONSE_BYTES = 16_777_216 +MAX_IMAGE_DECODED_BYTES = MAX_RESPONSE_BYTES REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") SHA_RE = re.compile(r"^[0-9a-f]{40}$") GITHUB_API_ORIGIN = "https://api.github.com" @@ -39,6 +41,7 @@ # (this org's own "attach the relevant paper PDF" convention) for a reason # that has nothing to do with the Nginx runtime policy this module enforces. BINARY_DOCUMENT_SUFFIXES = frozenset({".pdf"}) +DOCUMENT_ASSET_SUFFIXES = frozenset({".png"}) SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) @@ -228,6 +231,274 @@ def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: ) +def _is_documentation_image_path(path: str) -> bool: + """Return whether *path* claims to be raster evidence below documentation.""" + + pure = PurePosixPath(path) + return ( + len(pure.parts) > 1 + and any(part.lower() in DOCUMENTATION_DIRECTORIES for part in pure.parts[:-1]) + and pure.suffix.lower() in DOCUMENT_ASSET_SUFFIXES + ) + + +def _paeth_predictor(left: int, above: int, upper_left: int) -> int: + """Return the PNG Paeth predictor for three reconstructed bytes.""" + + estimate = left + above - upper_left + distances = (abs(estimate - left), abs(estimate - above), abs(estimate - upper_left)) + return (left, above, upper_left)[distances.index(min(distances))] + + +def _png_rows_are_valid( + pixels: bytes, + pass_layouts: list[tuple[int, int, int, int]], + *, + bytes_per_pixel: int, + bit_depth: int, + palette_entries: int | None, +) -> bool: + """Validate filters and indexed samples for every decoded PNG pass.""" + + for start, stride, rows, pass_width in pass_layouts: + previous = bytes(stride - 1) + for row in range(rows): + row_start = start + row * stride + filter_type = pixels[row_start] + if filter_type > 4: + return False + encoded = pixels[row_start + 1 : row_start + stride] + reconstructed = bytearray(len(encoded)) + for index, value in enumerate(encoded): + left = reconstructed[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + above = previous[index] + upper_left = previous[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + predictor = (0, left, above, (left + above) // 2, _paeth_predictor(left, above, upper_left))[filter_type] + reconstructed[index] = (value + predictor) & 0xFF + previous = bytes(reconstructed) + if palette_entries is None: + continue + samples_seen = 0 + for byte in reconstructed: + for shift in range(8 - bit_depth, -1, -bit_depth): + if samples_seen == pass_width: + break + if ((byte >> shift) & ((1 << bit_depth) - 1)) >= palette_entries: + return False + samples_seen += 1 + return True + + +def _is_recognized_documentation_image(path: str, raw: bytes) -> bool: + """Accept only bounded bytes with a structurally complete PNG envelope.""" + + suffix = PurePosixPath(path).suffix.lower() + if suffix != ".png" or len(raw) < 33 or not raw.startswith(b"\x89PNG\r\n\x1a\n"): + return False + offset = 8 + width = height = bit_depth = color_type = interlace = None + idat_parts: list[bytes] = [] + saw_plte = False + palette_entries = None + saw_trns = False + saw_idat = False + finished_idat = False + singleton_ancillary_chunks: set[bytes] = set() + while offset + 12 <= len(raw): + length = int.from_bytes(raw[offset : offset + 4], "big") + end = offset + 12 + length + if end > len(raw): + return False + chunk_type = raw[offset + 4 : offset + 8] + chunk_data = raw[offset + 8 : offset + 8 + length] + if ( + len(chunk_type) != 4 + or any(not (65 <= byte <= 90 or 97 <= byte <= 122) for byte in chunk_type) + or chunk_type[2] & 0x20 + ): + return False + if chunk_type[0] & 0x20 == 0 and chunk_type not in { + b"IHDR", + b"PLTE", + b"IDAT", + b"IEND", + }: + return False + declared_crc = int.from_bytes(raw[offset + 8 + length : end], "big") + if zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF != declared_crc: + return False + if offset == 8 and (chunk_type != b"IHDR" or length != 13): + return False + if chunk_type == b"IHDR": + if offset != 8 or width is not None: + return False + width = int.from_bytes(chunk_data[:4], "big") + height = int.from_bytes(chunk_data[4:8], "big") + bit_depth, color_type, compression, filtering, interlace = chunk_data[8:13] + if ( + width == 0 + or height == 0 + or width > MAX_IMAGE_DECODED_BYTES + or height > MAX_IMAGE_DECODED_BYTES + or compression != 0 + or filtering != 0 + or interlace not in {0, 1} + or bit_depth + not in { + 0: {1, 2, 4, 8, 16}, + 2: {8, 16}, + 3: {1, 2, 4, 8}, + 4: {8, 16}, + 6: {8, 16}, + }.get(color_type, set()) + ): + return False + elif chunk_type == b"PLTE": + if ( + width is None + or saw_plte + or saw_trns + or saw_idat + or color_type in {0, 4} + or length == 0 + or length % 3 + or length > 768 + or (color_type == 3 and length // 3 > 2**bit_depth) + ): + return False + saw_plte = True + palette_entries = length // 3 + elif chunk_type == b"tRNS": + sample_limit = (1 << bit_depth) - 1 if bit_depth is not None else -1 + valid_length = { + 0: length == 2 and int.from_bytes(chunk_data, "big") <= sample_limit, + 2: length == 6 + and all( + int.from_bytes(chunk_data[index : index + 2], "big") <= sample_limit + for index in (0, 2, 4) + ), + 3: saw_plte and 0 < length <= (palette_entries or 0), + }.get(color_type, False) + if width is None or saw_trns or saw_idat or not valid_length: + return False + saw_trns = True + elif chunk_type in {b"cHRM", b"gAMA", b"iCCP", b"sBIT", b"sRGB"}: + if ( + width is None + or saw_plte + or saw_idat + or chunk_type in singleton_ancillary_chunks + or (chunk_type == b"iCCP" and b"sRGB" in singleton_ancillary_chunks) + or (chunk_type == b"sRGB" and b"iCCP" in singleton_ancillary_chunks) + ): + return False + valid_ancillary = False + if chunk_type == b"cHRM": + valid_ancillary = length == 32 + elif chunk_type == b"gAMA": + valid_ancillary = length == 4 and int.from_bytes(chunk_data, "big") != 0 + elif chunk_type == b"sRGB": + valid_ancillary = length == 1 and chunk_data[0] <= 3 + elif chunk_type == b"iCCP": + separator = chunk_data.find(b"\x00") + valid_ancillary = ( + 1 <= separator <= 79 + and separator + 2 < length + and chunk_data[separator + 1] == 0 + ) + if valid_ancillary: + try: + profile_decoder = zlib.decompressobj() + profile = profile_decoder.decompress(chunk_data[separator + 2 :], MAX_IMAGE_DECODED_BYTES + 1) + profile += profile_decoder.flush() + except zlib.error: + valid_ancillary = False + else: + valid_ancillary = ( + profile_decoder.eof + and not profile_decoder.unused_data + and not profile_decoder.unconsumed_tail + and 0 < len(profile) <= MAX_IMAGE_DECODED_BYTES + ) + elif chunk_type == b"sBIT": + expected_length = {0: 1, 2: 3, 3: 3, 4: 2, 6: 4}.get(color_type) + sample_limit = 8 if color_type == 3 else bit_depth + valid_ancillary = ( + length == expected_length + and sample_limit is not None + and all(0 < value <= sample_limit for value in chunk_data) + ) + if not valid_ancillary: + return False + singleton_ancillary_chunks.add(chunk_type) + elif chunk_type == b"IDAT": + if width is None or finished_idat or (color_type == 3 and not saw_plte): + return False + saw_idat = True + idat_parts.append(chunk_data) + elif saw_idat: + finished_idat = True + if chunk_type == b"IEND": + if ( + length != 0 + or not saw_idat + or end != len(raw) + or width is None + or height is None + or (color_type == 3 and not saw_plte) + ): + return False + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] + passes = ( + [(0, 0, 1, 1)] + if interlace == 0 + else [ + (0, 0, 8, 8), + (4, 0, 8, 8), + (0, 4, 4, 8), + (2, 0, 4, 4), + (0, 2, 2, 4), + (1, 0, 2, 2), + (0, 1, 1, 2), + ] + ) + pass_layouts: list[tuple[int, int, int, int]] = [] + expected_size = 0 + for x_start, y_start, x_step, y_step in passes: + pass_width = 0 if width <= x_start else (width - x_start + x_step - 1) // x_step + pass_height = 0 if height <= y_start else (height - y_start + y_step - 1) // y_step + if not pass_width or not pass_height: + continue + row_bytes = (pass_width * channels * bit_depth + 7) // 8 + stride = row_bytes + 1 + pass_size = pass_height * stride + if pass_size > MAX_IMAGE_DECODED_BYTES - expected_size: + return False + pass_layouts.append((expected_size, stride, pass_height, pass_width)) + expected_size += pass_size + try: + decoder = zlib.decompressobj() + pixels = decoder.decompress(b"".join(idat_parts), expected_size + 1) + pixels += decoder.flush() + except zlib.error: + return False + return ( + decoder.eof + and not decoder.unused_data + and not decoder.unconsumed_tail + and len(pixels) == expected_size + and _png_rows_are_valid( + pixels, + pass_layouts, + bytes_per_pixel=max(1, (channels * bit_depth + 7) // 8), + bit_depth=bit_depth, + palette_entries=palette_entries if color_type == 3 else None, + ) + ) + offset = end + return False + + def _runtime_path_rule(path: str) -> str | None: """Return a path-level violation rule for active Nginx runtime artifacts.""" @@ -460,6 +731,8 @@ def _needs_content_scan(changed: ChangedFile) -> bool: if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): return False + if _is_documentation_image_path(changed.path): + return True if _is_binary_documentation_pdf(changed): return False if not changed.patch_available: @@ -499,6 +772,15 @@ def evaluate_pull_request( changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) violations: list[Violation] = [] for changed in changed_files: + if changed.status != "removed" and _is_documentation_image_path(changed.path): + raw = _load_raw_file_bytes( + api_url.rstrip("/"), repository, changed.path, head_sha, token, opener + ) + if not _is_recognized_documentation_image(changed.path, raw): + raise PolicyError( + f"Documentation image evidence for {changed.path} is not a recognized image" + ) + continue # A claimed binary documentation PDF gets its own network-verified # check ahead of _needs_content_scan's patch-presence-only signal: # a missing patch does not by itself prove binary content (GitHub diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 70bb1bc970..dae1fe30b5 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -39,6 +39,84 @@ def encoded_file(content: str, *, size: int | None = None, kind: str = "file", e } +def encoded_binary_file(raw: bytes) -> dict[str, object]: + """Build one GitHub Contents API response for binary evidence.""" + + return { + "type": "file", + "encoding": "base64", + "size": len(raw), + "content": base64.b64encode(raw).decode(), + } + + +def png_chunk(chunk_type: bytes, data: bytes = b"") -> bytes: + """Build one CRC-valid PNG chunk.""" + + chunk = len(data).to_bytes(4, "big") + chunk_type + data + return chunk + policy.zlib.crc32(chunk_type + data).to_bytes(4, "big") + + +def build_png( + *, + color_type: int, + bit_depth: int, + width: int = 2, + height: int = 2, + palette: bytes | None = None, +) -> bytes: + """Build a small structurally complete, non-interlaced PNG fixture.""" + + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] + row_bytes = (width * channels * bit_depth + 7) // 8 + pixels = b"".join(b"\x00" + bytes(row_bytes) for _ in range(height)) + ihdr = ( + width.to_bytes(4, "big") + + height.to_bytes(4, "big") + + bytes((bit_depth, color_type, 0, 0, 0)) + ) + chunks = [png_chunk(b"IHDR", ihdr)] + if palette is not None: + chunks.append(png_chunk(b"PLTE", palette)) + chunks.extend((png_chunk(b"IDAT", policy.zlib.compress(pixels)), png_chunk(b"IEND"))) + return b"\x89PNG\r\n\x1a\n" + b"".join(chunks) + + +def insert_png_chunk(raw: bytes, chunk_type: bytes, data: bytes = b"") -> bytes: + """Insert one CRC-valid chunk immediately before the first IDAT.""" + + idat_offset = raw.index(b"IDAT") - 4 + return raw[:idat_offset] + png_chunk(chunk_type, data) + raw[idat_offset:] + + +def replace_png_ihdr(raw: bytes, **fields: int) -> bytes: + """Return *raw* with selected IHDR fields and a matching CRC.""" + + image = bytearray(raw) + positions = { + "width": (16, 20), + "height": (20, 24), + "bit_depth": (24, 25), + "color_type": (25, 26), + "interlace": (28, 29), + } + for name, value in fields.items(): + start, end = positions[name] + image[start:end] = value.to_bytes(end - start, "big") + image[29:33] = policy.zlib.crc32(image[12:29]).to_bytes(4, "big") + return bytes(image) + + +def replace_png_chunk(raw: bytes, chunk_type: bytes, data: bytes) -> bytes: + """Replace the first named PNG chunk with CRC-valid *data*.""" + + type_offset = raw.index(chunk_type, 8) + chunk_offset = type_offset - 4 + old_length = int.from_bytes(raw[chunk_offset:type_offset], "big") + old_end = type_offset + 8 + old_length + return raw[:chunk_offset] + png_chunk(chunk_type, data) + raw[old_end:] + + def test_scan_content_rejects_runtime_paths_and_every_denied_runtime_form() -> None: """Runtime filenames and all supported active Nginx forms fail closed.""" @@ -97,6 +175,264 @@ def test_nested_documentation_path_allows_prose_samples() -> None: assert policy.scan_content("packages/component/docs/migration.md", fixture_text()) == () +@pytest.mark.parametrize( + ("color_type", "bit_depth"), + [ + (0, 1), (0, 2), (0, 4), (0, 8), (0, 16), + (2, 8), (2, 16), + (3, 1), (3, 2), (3, 4), (3, 8), + (4, 8), (4, 16), + (6, 8), (6, 16), + ], +) +def test_documentation_png_accepts_every_legal_color_and_depth_pair( + color_type: int, bit_depth: int +) -> None: + """The bounded validator accepts each PNG-defined color/depth combination.""" + + palette = b"\x00\x00\x00" if color_type == 3 else None + raw = build_png(color_type=color_type, bit_depth=bit_depth, palette=palette) + assert policy._is_recognized_documentation_image("docs/acceptance.png", raw) + + +def test_documentation_png_rejects_disguised_text_and_invalid_structure() -> None: + """A suffix, corrupt stream, invalid palette, or reserved chunk bit is insufficient.""" + + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", b"#!/bin/sh\nsystemctl restart nginx\n" + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", build_png(color_type=3, bit_depth=1) + ) + rgba = build_png(color_type=6, bit_depth=8, palette=b"\x00\x00\x00") + assert policy._is_recognized_documentation_image("docs/acceptance.png", rgba) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(rgba, b"PLTE", b"\xff\xff\xff") + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(rgba, b"abca") + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(rgba, b"tRNS", b"\x00") + ) + grayscale = build_png(color_type=0, bit_depth=8) + assert policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(grayscale, b"tRNS", b"\x00\x00") + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(grayscale, b"tRNS", b"\x01\x00") + ) + truecolor = build_png(color_type=2, bit_depth=8) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", + insert_png_chunk(truecolor, b"tRNS", b"\x00\x00\x00\x00\x01\x00"), + ) + truecolor_with_palette_after_transparency = insert_png_chunk( + insert_png_chunk(truecolor, b"tRNS", b"\x00" * 6), + b"PLTE", + b"\x00\x00\x00", + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", truecolor_with_palette_after_transparency + ) + + +@pytest.mark.parametrize("filter_type", [0, 1, 2, 3, 4]) +def test_documentation_png_reconstructs_each_legal_filter(filter_type: int) -> None: + """Every standard PNG scanline filter is decoded before acceptance.""" + + raw = build_png(color_type=6, bit_depth=8, width=1, height=1) + raw = replace_png_chunk(raw, b"IDAT", policy.zlib.compress(bytes((filter_type, 0, 0, 0, 0)))) + assert policy._is_recognized_documentation_image("docs/acceptance.png", raw) + + +def test_documentation_png_rejects_unknown_filter() -> None: + """A structurally sized stream still fails on an unknown filter byte.""" + + raw = build_png(color_type=6, bit_depth=8, width=1, height=1) + raw = replace_png_chunk(raw, b"IDAT", policy.zlib.compress(b"\x05\x00\x00\x00\x00")) + assert not policy._is_recognized_documentation_image("docs/acceptance.png", raw) + + +@pytest.mark.parametrize("filter_type", [0, 1]) +@pytest.mark.parametrize("interlace", [0, 1]) +def test_documentation_png_rejects_missing_palette_entries( + filter_type: int, interlace: int +) -> None: + """Indexed pixels cannot reference an entry absent from PLTE.""" + + raw = build_png( + color_type=3, + bit_depth=8, + width=1, + height=1, + palette=b"\x00\x00\x00", + ) + raw = replace_png_chunk(raw, b"IDAT", policy.zlib.compress(bytes((filter_type, 1)))) + if interlace: + raw = replace_png_ihdr(raw, interlace=1) + assert not policy._is_recognized_documentation_image("docs/acceptance.png", raw) + + +def test_documentation_image_path_requires_a_documentation_directory() -> None: + """Root-level image names do not cross the documented directory boundary.""" + + assert policy._is_documentation_image_path("docs/screenshots/acceptance.png") + assert not policy._is_documentation_image_path("README.png") + assert not policy._is_documentation_image_path("CHANGELOG.png") + + +def test_documentation_png_rejects_each_malformed_envelope_boundary() -> None: + """Chunk bounds, CRCs, ordering, dimensions, streams, and trailers fail closed.""" + + raw = build_png(color_type=6, bit_depth=8, width=1, height=1) + oversized_chunk = bytearray(raw) + oversized_chunk[8:12] = (999).to_bytes(4, "big") + corrupt_crc = bytearray(raw) + corrupt_crc[29] ^= 1 + first_chunk_not_ihdr = bytearray(raw) + first_chunk_not_ihdr[12:16] = b"IDAT" + first_chunk_not_ihdr[29:33] = policy.zlib.crc32( + first_chunk_not_ihdr[12:29] + ).to_bytes(4, "big") + duplicate_ihdr = insert_png_chunk(raw, b"IHDR", raw[16:29]) + unknown_critical = insert_png_chunk(raw, b"ABCD") + invalid_ihdr = replace_png_ihdr(raw, color_type=1) + malformed_zlib = replace_png_chunk(raw, b"IDAT", b"not-zlib") + iend_offset = raw.index(b"IEND") - 4 + split_idat = raw[:iend_offset] + png_chunk(b"tEXt") + raw[raw.index(b"IDAT") - 4 : iend_offset] + raw[iend_offset:] + + for candidate in ( + bytes(oversized_chunk), + bytes(corrupt_crc), + bytes(first_chunk_not_ihdr), + duplicate_ihdr, + unknown_critical, + invalid_ihdr, + malformed_zlib, + split_idat, + raw + b"trailing", + raw[:-12], + replace_png_ihdr(raw, width=policy.MAX_IMAGE_DECODED_BYTES), + ): + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", candidate + ) + + assert policy._is_recognized_documentation_image( + "docs/acceptance.png", replace_png_ihdr(raw, interlace=1) + ) + after_idat = raw[:iend_offset] + png_chunk(b"tEXt") + raw[iend_offset:] + assert policy._is_recognized_documentation_image( + "docs/acceptance.png", after_idat + ) + assert policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(raw, b"tEXt") + ) + + +@pytest.mark.parametrize( + ("chunk_type", "chunk_data"), + [ + (b"cHRM", b"\x00" * 32), + (b"gAMA", (45455).to_bytes(4, "big")), + (b"iCCP", b"profile\x00\x00" + policy.zlib.compress(b"icc-profile")), + (b"sBIT", b"\x08" * 4), + (b"sRGB", b"\x00"), + ], +) +def test_documentation_png_enforces_standard_ancillary_order_and_cardinality( + chunk_type: bytes, chunk_data: bytes +) -> None: + """Color-space and significant-bit chunks occur once before image data.""" + + raw = build_png(color_type=6, bit_depth=8, width=1, height=1) + before_idat = insert_png_chunk(raw, chunk_type, chunk_data) + assert policy._is_recognized_documentation_image( + "docs/acceptance.png", before_idat + ) + + iend_offset = raw.index(b"IEND") - 4 + after_idat = raw[:iend_offset] + png_chunk(chunk_type, chunk_data) + raw[iend_offset:] + duplicate = insert_png_chunk(before_idat, chunk_type, chunk_data) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", after_idat + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", duplicate + ) + + +def test_documentation_png_rejects_conflicting_or_malformed_color_chunks() -> None: + """Color-space chunks fail closed on conflicts and malformed payloads.""" + + raw = build_png(color_type=6, bit_depth=8, width=1, height=1) + srgb = insert_png_chunk(raw, b"sRGB", b"\x00") + conflicting = insert_png_chunk( + srgb, b"iCCP", b"profile\x00\x00" + policy.zlib.compress(b"icc-profile") + ) + for chunk_type, chunk_data in ( + (b"cHRM", b"\x00" * 31), + (b"gAMA", b"\x00" * 4), + (b"iCCP", b"profile\x00\x00not-zlib"), + (b"sBIT", b"\x09" * 4), + (b"sRGB", b"\x04"), + ): + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", insert_png_chunk(raw, chunk_type, chunk_data) + ) + assert not policy._is_recognized_documentation_image( + "docs/acceptance.png", conflicting + ) + + +def test_documentation_png_always_requires_final_byte_validation() -> None: + """Recognized documentation PNG paths remain final-content scan candidates.""" + + assert policy._needs_content_scan( + policy.ChangedFile( + "docs/screenshots/acceptance.png", "added", "", patch_available=False + ) + ) + + +def test_evaluate_pull_request_accepts_only_recognized_documentation_png() -> None: + """The live boundary verifies final PNG bytes before granting the documentation exception.""" + + raw = build_png(color_type=6, bit_depth=8) + + def accepted(url: str, _token: str) -> object: + if "/pulls/18/files" in url: + return [{"filename": "docs/screenshots/acceptance.png", "status": "added"}] + return encoded_binary_file(raw) + + assert policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=18, + head_sha="d" * 40, + event_action="opened", + token="token", + opener=accepted, + ) == () + + def disguised(url: str, _token: str) -> object: + if "/pulls/19/files" in url: + return [{"filename": "docs/screenshots/acceptance.png", "status": "added"}] + return encoded_file("systemctl restart nginx\n") + + with pytest.raises(policy.PolicyError, match="recognized image"): + policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=19, + head_sha="e" * 40, + event_action="opened", + token="token", + opener=disguised, + ) + + def test_needs_content_scan_exempts_documentation_pdfs() -> None: """A cited research-paper PDF under docs/ never reaches content scanning.