#!/usr/bin/env bash # check-binary-composition.sh — release gate: assert what must NOT be inside a # shipped artifact. # # Microsoft Defender's ML classifier flagged the v0.9.1-rc.1 binaries. The # hardening pass that followed removed capability or opaque asset bytes — an # executable stack, an in-process updater, test-only environment seams, # embedded integration programs, and the in-image frontend bundle. Each of # those regresses invisibly: one restored #include, one Makefile source-list # edit, one revived call site, and nothing else in CI notices — the binary just # quietly restores capability or opaque bytes that this hardening boundary is # intended to exclude. Their removal reduces attack surface and makes release # contents independently inspectable; it does not establish which feature, if # any, caused an opaque third-party ML verdict. # # This script is the proof that each removal stayed removed. It asserts only # NEGATIVE properties (needle absent), plus one canary string we know ships, # because an absence check aimed at the wrong file — a compressed artifact, a # stub, a truncated download — would otherwise pass vacuously and read green. # A missing tool is a hard error for the same reason: a skipped assertion must # never look like a satisfied one. # # Usage: scripts/ci/check-binary-composition.sh [--variant=auto|standard|ui] ... # Directories are scanned recursively; format (ELF / Mach-O / PE) is detected # per file from its magic bytes and each assertion runs where it is # meaningful. Exit 0 = every assertion passed, 1 = at least one failed, # 2 = usage error, missing tool, or nothing checkable was found (a vacuous # run is a failure, not a pass). set -euo pipefail case "${1:-}" in -h | --help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; esac # ── Needles ───────────────────────────────────────────────────────── # Every needle below was grep-verified against the tree it comes from: it # exists ONLY in the code the assertion is about, so a hit means that code is # linked in, not that some unrelated string happens to look similar. # S6: worker/Windows test seams read these env vars. A release binary that # still honours them lets any process on the box steer the indexer's child # processes and file placement. Their absence is a release-security boundary; # no claim is made about whether a third-party classifier weighs them. SEAM_NEEDLES=( 'CBM_TEST_WORKER_DESCENDANT_PID_FILE' 'CBM_TEST_CRASH_ON' 'CBM_TEST_HANG_ON' ) # The in-process updater combined network download with replacement of its own # executable. That dual-use behavior is no longer needed in the daemon; updates # now run from install.sh out-of-process. These four needles cover both halves # of what was removed: # the download base URL, the checksum URL built on top of it, the GitHub API # release query of the daemon's background version check, and that request's # Accept header (which survives even if the URL is ever assembled at runtime). UPDATER_NEEDLES=( 'releases/latest/download' 'api.github.com/repos' 'releases/latest' 'Accept: application/vnd.github+json' ) # SQLite loadable extensions = arbitrary code execution through a database # file. The amalgamation is built with SQLITE_OMIT_LOAD_EXTENSION; if that ever # drops out, dlopen()/LoadLibrary() re-enters the store layer. # NOTE: release binaries are stripped, so the two API symbol names only appear # when the surface is *exported*. The dlopen error text is the needle that # still works on a stripped build — it is compiled in only when the feature is. SQLITE_LOADEXT_NEEDLES=( 'sqlite3_load_extension' 'sqlite3_enable_load_extension' 'unable to open shared library [' ) # The standard artifact must not contain the UI's HTTP server at all: an # unauthenticated localhost listener plus a process enumerator is unnecessary # capability in the standard composition. Needles are split across # both UI translation units and are independent of each other, so a refactor of # any single one cannot silently disarm the assertion: # 'HTTP/1.1 %d %s' httpd.c response-status writer # 'Request Header Fields Too Large' httpd.c status-text table # '/api/ui-config', '/api/processes' http_server.c route dispatch # 'ps -eo …' / '[c]odebase-memory-mcp' the popen() process enumerator's # shell pipeline, both halves # Verified unique to src/ui/ across src/, vendored/ and internal/ — the only # other occurrences in the repo are tests/test_httpd.c and scripts/, neither of # which is ever linked into a release binary. UI_HTTP_NEEDLES=( 'HTTP/1.1 %d %s' 'Request Header Fields Too Large' '/api/ui-config' '/api/processes' 'ps -eo pid,pcpu,rss,etime,comm' '[c]odebase-memory-mcp' ) # The binary used to embed nine complete shebang'd shell scripts, their # PowerShell/.cmd twins and two node:child_process client modules — script # bodies written to disk at 0755. Keeping those programs outside the native # image narrows executable surface and lets each component be inspected and # scanned independently. They now ship in # cbm-integrations.json (the binary embeds only its SHA-256); these needles # prove the removal stayed removed. Source-level enforcement lives in # tests/test_no_embedded_scripts_contract.sh; this is the shipped-artifact # half of the same property. SCRIPT_NEEDLES=( '#!/usr/bin/env bash' '#!/bin/bash' '#!/bin/sh' 'node:child_process' ) # UI frontend bytes belong exclusively in the independently scannable pack. # These are structural bytes from the pack header and Vite's emitted index, # selected to avoid generic HTML grammar strings that legitimately occur in the # language parsers linked into every binary. UI_ASSET_BYTE_NEEDLES=( 'CBMUIPK' '' '' '