mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-24 06:23:06 +08:00
Microsoft's ML flagged the rc.1 release binaries. The decisive evidence is that the SAME sha256 went from 0/62 clean to Microsoft-detected in about an hour with no byte change, so the verdict lives partly in scanner-side state and no code change can promise a clean result. What code CAN do is stop shipping things that have no business in a release artifact, which is worth doing on its own merits and incidentally widens the classifier margin. Every claim below is verified against a built binary by the new gate, not by reading source. Executable stack (the worst of the findings). vendored/nomic/code_vectors_blob.S is the only assembly in the build and carried no .note.GNU-stack. An unannotated object makes ld assume the worst for the whole link, so EVERY Linux release we have ever shipped had GNU_STACK RWE. Adds the note (cause) plus ELF-only -Wl,-z,noexecstack (outcome); the gate fails the release if it returns. Test seams are now opt-in, never opt-out. TEST_SEAMS=1 defines CBM_ENABLE_TEST_SEAMS; without it the crash-orphan probe -- which forks a child that ignores SIGTERM and loops forever, then writes its pid to a caller-supplied path -- and the lease-ownership marker compile to trivial stubs, so call sites are untouched and the binary holds no fork, no signal handler and no env-var string. Opt-IN is the point: forgetting the flag yields a clean binary rather than a leaky one. scripts/test.sh requests it in the leg that consumes it, and tests/test_worker_watchdog.sh now asserts the capability up front instead of dying later with an opaque "Killed: 9". The daemon's background version check is gone. It spawned curl against api.github.com/repos/.../releases/latest on the first eligible session of every run to say "a newer version exists" -- a release URL and an outbound request in every shipped binary, for something the install scripts already report. The INJECTABLE SEAM survives: update_ops is still honoured, the fakes in tests/test_daemon_application.c still cover notice/ownership/cancellation/replay, and with no provider application_update_subscribe_locked returns early so no generation ever starts. "No network request by default" is now structural. Dead capability out of release builds. The tar.gz/zip extraction block (gzip_decompress through cbm_extract_binary_from_zip, plus its cli.h declarations) moves under CBM_CLI_ENABLE_TEST_API -- verified self-contained, zero uses of any helper outside it, only callers the excluded updater and tests/test_cli.c. Downloading an archive, decompressing it, picking an executable out of it and marking it executable is the canonical dropper composite; it is now absent rather than merely unreachable. SQLite is built with -DSQLITE_OMIT_LOAD_EXTENSION (no caller of load_extension anywhere in src/ or internal/), removing that API surface and part of the dlopen/dlsym surface. Temp files and environment scanning (S2/S3). Predictable paths in mcp.c, artifact.c and diagnostics.c are created privately and exclusively and written through the returned descriptor; pass_envscan.c no longer descends symlinked directories out of the project root, and its fixed 512-byte path buffers no longer truncate into pointer arithmetic that could land outside the buffer. Build-time entropy. mimalloc's version banner baked __DATE__/__TIME__ into every binary, so two builds of identical source seconds apart could never share a hash and no release could inherit a false-positive determination made about its predecessor. Local patch removes it (marked to survive refreshes), -Wdate-time makes any future use a build error, and -Wl,--no-insert-timestamp stops the PE header carrying the link clock. scripts/ci/check-binary-composition.sh is the proof that each removal stays removed, wired into package-release.sh after strip so the local artifact-flow smoke enforces exactly what CI does. It asserts absences plus a CANARY string, so handing it a compressed, stubbed or empty file fails instead of passing vacuously, and a missing tool is a hard error -- a skipped assertion must never look like a satisfied one. Two build-system traps found by that gate, both of which had silently defeated a fix: the product binary is compiled in one shot from sources, so a flag flip did not rebuild it (now tracked by a .build-config stamp that also removes the binary, making it independent of mtime granularity); and prod_sqlite3.o / prod_mimalloc.o depended on a single named source, so SQLITE_OMIT_LOAD_EXTENSION and the mimalloc patch BOTH compiled to nothing on the first incremental build. Source review would have called them done. Deliberately NOT changed. Three seams stay in release artifacts because scripts/smoke-test.sh runs against the real artifact and needs them: CBM_TEST_CRASH_ON and CBM_TEST_HANG_ON inject the faults that prove supervisor recovery, and CBM_TEST_WINDOWS_USER_PATH_RUN_ID is what keeps the PATH smoke from writing the tester's actual PATH. The gate treats those as an allowlist, so a NOVEL seam still fails. The true no-UI standard build is deferred rather than rushed: src/ui/* is in PROD_SRCS and four files outside src/ui reference UI symbols, including the daemon that serves the UI, so that assertion reports instead of failing until the split lands -- a gate everyone knows is red teaches people to ignore gates. No grammar is removed. ObjectScript accounts for essentially all binary growth since the last provably-clean release (+21.4MB rodata, +1.1MB text from two four-line shims), which made it the obvious ablation candidate, but a dry run performed twelve real Defender endpoint scans across standard/UI and amd64/arm64 with ObjectScript, the daemon and the expanded hooks all present and every scan was clean. Nothing there is a deterministic trigger, so cutting a community-contributed language would spend a real feature on unproven margin. Lean is not a candidate either: at 99.6MB of source it is by far the largest grammar, but it shipped in v0.9.0 which scanned 20/20 clean, so removing it would produce a novel unscanned profile instead of restoring a known-good one. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
405 lines
18 KiB
Bash
405 lines
18 KiB
Bash
#!/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* — an executable stack, an
|
|
# in-process updater, test-only environment seams, the embedded HTTP/UI server
|
|
# and its process enumerator in the standard build. 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 gets its
|
|
# malware-shaped surface back and the next release gets flagged again.
|
|
#
|
|
# 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] <binary-or-dir>...
|
|
# 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 — and CBM_TEST_* in a shipped binary is exactly
|
|
# the "debug/injection hooks" shape AV heuristics score on.
|
|
SEAM_NEEDLES=(
|
|
'CBM_TEST_WORKER_DESCENDANT_PID_FILE'
|
|
'CBM_TEST_WINDOWS_USER_PATH_RUN_ID'
|
|
)
|
|
|
|
# The in-process updater (download-and-replace-own-binary) is the single most
|
|
# malware-shaped behaviour we ever shipped; 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 precisely the
|
|
# behaviour pair a classifier reads as a backdoor. 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'
|
|
)
|
|
|
|
# Canary: proves the needle scan can actually see this file's strings. Without
|
|
# it, handing the gate a gzip, a stub or a 0-byte file would pass every
|
|
# absence assertion. 168+ occurrences in a real artifact, 0 in anything else.
|
|
CANARY_NEEDLE='codebase-memory-mcp'
|
|
|
|
# ── Args ────────────────────────────────────────────────────────────
|
|
VARIANT=auto
|
|
TARGETS=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--variant=*) VARIANT="${arg#--variant=}" ;;
|
|
-*)
|
|
echo "FAIL: unknown flag $arg (see --help)" >&2
|
|
exit 2
|
|
;;
|
|
*) TARGETS+=("$arg") ;;
|
|
esac
|
|
done
|
|
case "$VARIANT" in
|
|
auto | standard | ui) ;;
|
|
*)
|
|
echo "FAIL: --variant must be auto, standard or ui (got '$VARIANT')" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
|
echo "FAIL: no binaries or directories given (see --help)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# ── ELF program-header reader ───────────────────────────────────────
|
|
# Resolved lazily: a Windows/macOS-only run must not fail for want of readelf,
|
|
# but an ELF in scope with no reader available must fail loudly (exit 2) rather
|
|
# than skip the executable-stack assertion.
|
|
ELF_READER=''
|
|
ELF_READER_KIND=''
|
|
resolve_elf_reader() {
|
|
[ -n "$ELF_READER" ] && return 0
|
|
for cand in readelf llvm-readelf /opt/homebrew/opt/llvm/bin/llvm-readelf \
|
|
/usr/local/opt/llvm/bin/llvm-readelf; do
|
|
if command -v "$cand" >/dev/null 2>&1; then
|
|
ELF_READER="$cand"
|
|
ELF_READER_KIND=readelf
|
|
return 0
|
|
fi
|
|
done
|
|
for cand in objdump llvm-objdump /opt/homebrew/opt/llvm/bin/llvm-objdump; do
|
|
if command -v "$cand" >/dev/null 2>&1; then
|
|
ELF_READER="$cand"
|
|
ELF_READER_KIND=objdump
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Echoes the GNU_STACK flag field ("RWE", "RW", "rwx", "rw-"), empty if the
|
|
# header is absent or unparseable.
|
|
gnu_stack_flags() {
|
|
case "$ELF_READER_KIND" in
|
|
readelf)
|
|
"$ELF_READER" -lW "$1" 2>/dev/null |
|
|
awk '/GNU_STACK/ { print $(NF - 1); exit }'
|
|
;;
|
|
objdump)
|
|
# objdump -p prints " STACK off ..." and the flags on the next line.
|
|
"$ELF_READER" -p "$1" 2>/dev/null |
|
|
awk '/STACK off/ { getline
|
|
for (i = 1; i <= NF; i++)
|
|
if ($i == "flags") { print $(i + 1); exit } }'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Reporting ───────────────────────────────────────────────────────
|
|
# PASS and FAIL both go to stdout so the per-assertion sequence stays in order
|
|
# in a CI log (stderr would interleave nondeterministically); only the final
|
|
# verdict is echoed to stderr, which is what a failed step's tail shows.
|
|
pass_count=0
|
|
fail_count=0
|
|
FAILURES=()
|
|
|
|
report() { # verdict, assertion-id, file-token, message
|
|
printf '%-4s %-22s %s: %s\n' "$1" "$2" "$3" "$4"
|
|
if [ "$1" = FAIL ]; then
|
|
fail_count=$((fail_count + 1))
|
|
FAILURES+=("$2 $3: $4")
|
|
else
|
|
pass_count=$((pass_count + 1))
|
|
fi
|
|
}
|
|
|
|
assert_absent() { # file, token, assertion-id, needle
|
|
if LC_ALL=C grep -a -q -F -e "$4" "$1"; then
|
|
report FAIL "$3" "$2" "'$4' is PRESENT (must not ship)"
|
|
else
|
|
report PASS "$3" "$2" "'$4' absent"
|
|
fi
|
|
}
|
|
|
|
assert_present() { # file, token, assertion-id, needle, why-pass, why-fail
|
|
if LC_ALL=C grep -a -q -F -e "$4" "$1"; then
|
|
report PASS "$3" "$2" "'$4' present ($5)"
|
|
else
|
|
report FAIL "$3" "$2" "'$4' is MISSING — $6"
|
|
fi
|
|
}
|
|
|
|
# ── Per-file checks ─────────────────────────────────────────────────
|
|
detect_format() { # file → elf | macho | pe | other
|
|
magic=$(LC_ALL=C od -An -N4 -tx1 "$1" 2>/dev/null | tr -d ' \n')
|
|
case "$magic" in
|
|
7f454c46) echo elf ;;
|
|
cffaedfe | cefaedfe | feedface | feedfacf | cafebabe | bebafeca) echo macho ;;
|
|
4d5a*) echo pe ;;
|
|
*) echo other ;;
|
|
esac
|
|
}
|
|
|
|
checked_files=0
|
|
skipped_files=0
|
|
|
|
check_file() {
|
|
file="$1"
|
|
# Two path components: both variants ship a binary literally named
|
|
# "codebase-memory-mcp", so the parent directory is what tells them apart.
|
|
token=$(printf '%s' "$file" | awk -F/ '{ if (NF > 1) print $(NF - 1) "/" $NF; else print $NF }')
|
|
fmt=$(detect_format "$file")
|
|
if [ "$fmt" = other ]; then
|
|
printf 'skip %-22s %s: not an ELF/Mach-O/PE binary\n' '-' "$token"
|
|
skipped_files=$((skipped_files + 1))
|
|
return 0
|
|
fi
|
|
|
|
is_ui=0
|
|
case "$VARIANT" in
|
|
ui) is_ui=1 ;;
|
|
standard) is_ui=0 ;;
|
|
auto)
|
|
# The UI archive is codebase-memory-mcp-ui-<os>-<arch>, but the binary
|
|
# inside it is just "codebase-memory-mcp" — so the whole path decides,
|
|
# not the basename. Printed below so a misclassified path is visible in
|
|
# the log instead of silently disarming the UI assertion.
|
|
case "$file" in
|
|
*-ui | *-ui.* | *-ui-* | *-ui/*) is_ui=1 ;;
|
|
esac
|
|
;;
|
|
esac
|
|
|
|
variant_label=standard
|
|
[ "$is_ui" -eq 1 ] && variant_label='UI (A5-no-ui-http not applicable)'
|
|
printf '\n── %s [%s, %s] ──\n' "$token" "$fmt" "$variant_label"
|
|
checked_files=$((checked_files + 1))
|
|
|
|
# A0 — anti-vacuity canary; every assertion below is an absence check.
|
|
assert_present "$file" "$token" A0-canary "$CANARY_NEEDLE" \
|
|
'the needle scan can read this file' \
|
|
'this is not one of our artifacts, or its strings are unreadable (packed/compressed/truncated) — every absence assertion below would pass vacuously'
|
|
|
|
# A1 — executable stack (ELF only). Every Linux artifact of v0.9.1-rc.1
|
|
# shipped GNU_STACK RWE: a writable+executable stack, which no modern
|
|
# binary has and which any classifier weighs heavily.
|
|
if [ "$fmt" = elf ]; then
|
|
if ! resolve_elf_reader; then
|
|
echo "FAIL: no readelf/llvm-readelf/objdump available; cannot assert" \
|
|
"the non-executable stack for $token — refusing to skip it" >&2
|
|
exit 2
|
|
fi
|
|
flags=$(gnu_stack_flags "$file")
|
|
# readelf spells the flags "RWE"/"RW", objdump "rwx"/"rw-" — match both.
|
|
case "$flags" in
|
|
'')
|
|
report FAIL A1-noexec-stack "$token" \
|
|
"no GNU_STACK program header found ($ELF_READER) — without PT_GNU_STACK the loader may fall back to READ_IMPLIES_EXEC"
|
|
;;
|
|
*E* | *X* | *e* | *x*)
|
|
report FAIL A1-noexec-stack "$token" \
|
|
"GNU_STACK is $flags — the stack is EXECUTABLE (link with -z noexecstack; check .S/asm objects for a missing .note.GNU-stack)"
|
|
;;
|
|
*)
|
|
report PASS A1-noexec-stack "$token" "GNU_STACK is $flags (no execute bit)"
|
|
;;
|
|
esac
|
|
else
|
|
printf 'n/a %-22s %s: executable-stack check is ELF-only\n' A1-noexec-stack "$token"
|
|
fi
|
|
|
|
# A2 — test-only seams.
|
|
for needle in "${SEAM_NEEDLES[@]}"; do
|
|
assert_absent "$file" "$token" A2-no-test-seams "$needle"
|
|
done
|
|
# Sweep for seams nobody thought to pin: a new CBM_TEST_* env var added to
|
|
# production code lands here on its first release, not on the next audit.
|
|
# Two seams remain in release artifacts BY DECISION, so the wildcard is an
|
|
# allowlist rather than a blanket ban: scripts/smoke-test.sh runs against the
|
|
# real release artifact, and these are what let it do so honestly —
|
|
# CRASH_ON/HANG_ON inject the faults that prove supervisor recovery, and
|
|
# WINDOWS_USER_PATH_RUN_ID is what stops the PATH smoke from writing the
|
|
# tester's actual PATH. Deleting them would trade genuine release-artifact
|
|
# coverage for a cosmetic win. Anything NOT on this list is a novel seam and
|
|
# fails, which is the property that matters: the list can only shrink by
|
|
# decision, never grow by accident.
|
|
seam_allowed='CBM_TEST_CRASH_ON CBM_TEST_HANG_ON CBM_TEST_WINDOWS_USER_PATH_RUN_ID'
|
|
seam_unexpected=''
|
|
for found in $(LC_ALL=C grep -a -o -E 'CBM_TEST_[A-Za-z0-9_]+' "$file" 2>/dev/null |
|
|
sort -u || true); do
|
|
case " $seam_allowed " in
|
|
*" $found "*) ;;
|
|
*) seam_unexpected="$seam_unexpected $found" ;;
|
|
esac
|
|
done
|
|
if [ -n "$seam_unexpected" ]; then
|
|
report FAIL A2-no-test-seams "$token" \
|
|
"unexpected CBM_TEST_* seam(s):$seam_unexpected (allowlist: $seam_allowed)"
|
|
else
|
|
report PASS A2-no-test-seams "$token" 'no CBM_TEST_* seams beyond the smoke allowlist'
|
|
fi
|
|
|
|
# A3 — updater/release URLs.
|
|
for needle in "${UPDATER_NEEDLES[@]}"; do
|
|
assert_absent "$file" "$token" A3-no-updater-urls "$needle"
|
|
done
|
|
|
|
# A4 — SQLite loadable-extension surface.
|
|
for needle in "${SQLITE_LOADEXT_NEEDLES[@]}"; do
|
|
assert_absent "$file" "$token" A4-no-sqlite-loadext "$needle"
|
|
done
|
|
# Positive control for A4: the three needles above are all *absent* on a
|
|
# stripped build even when the feature is compiled in, so on its own that
|
|
# assertion can pass vacuously. SQLite's own compile-option table names
|
|
# every OMIT_* it was built with — when that table is present (it ships
|
|
# unless SQLITE_OMIT_COMPILEOPTION_DIAGS is set) it proves the omission
|
|
# positively rather than by absence.
|
|
if LC_ALL=C grep -a -q -F -e 'sqlite_compileoption_get' "$file"; then
|
|
assert_present "$file" "$token" A4-no-sqlite-loadext 'OMIT_LOAD_EXTENSION' \
|
|
'sqlite reports SQLITE_OMIT_LOAD_EXTENSION' \
|
|
'sqlite was NOT built with SQLITE_OMIT_LOAD_EXTENSION, so the dlopen()/LoadLibrary() extension loader is linked in'
|
|
else
|
|
printf 'n/a %-22s %s: %s\n' A4-no-sqlite-loadext "$token" \
|
|
"sqlite compile-option table absent; A4 rests on absence needles only"
|
|
fi
|
|
|
|
# A5 — UI/HTTP subsystem, standard artifacts only.
|
|
#
|
|
# NOT YET ENFORCED. src/ui/{config,http_server,layout3d,httpd,embedded_stub}.c
|
|
# are still in PROD_SRCS, and the standard build merely substitutes empty
|
|
# embedded assets — so the HTTP server and its popen("ps … | grep") process
|
|
# enumerator are linked into artifacts that can never serve a UI. Excluding
|
|
# them is a real refactor: four files outside src/ui (src/main.c,
|
|
# src/daemon/host.c, src/daemon/application.c, src/mcp/index_supervisor.c)
|
|
# reference UI symbols, including the daemon that serves the UI.
|
|
#
|
|
# Until that lands, this assertion reports rather than fails. A gate everyone
|
|
# knows is red teaches people to ignore gates, and a known-failing check has
|
|
# no more enforcement value than this note — but the needles are kept live and
|
|
# exercised so that the day the split lands, flipping CBM_CHECK_UI_ABSENT=1
|
|
# (then making it the default) is a one-line change and not a rewrite.
|
|
if [ "$is_ui" -eq 1 ]; then
|
|
printf 'n/a %-22s %s: UI artifact, HTTP server ships here by design\n' \
|
|
A5-no-ui-http "$token"
|
|
elif [ "${CBM_CHECK_UI_ABSENT:-0}" = "1" ]; then
|
|
for needle in "${UI_HTTP_NEEDLES[@]}"; do
|
|
assert_absent "$file" "$token" A5-no-ui-http "$needle"
|
|
done
|
|
else
|
|
ui_hits=0
|
|
for needle in "${UI_HTTP_NEEDLES[@]}"; do
|
|
if LC_ALL=C grep -a -q -F -e "$needle" "$file"; then
|
|
ui_hits=$((ui_hits + 1))
|
|
fi
|
|
done
|
|
printf 'INFO %-22s %s: %d/%d UI/HTTP needles present (no-UI split pending; set CBM_CHECK_UI_ABSENT=1 to enforce)\n' \
|
|
A5-no-ui-http "$token" "$ui_hits" "${#UI_HTTP_NEEDLES[@]}"
|
|
fi
|
|
}
|
|
|
|
# ── Walk the targets ────────────────────────────────────────────────
|
|
for target in "${TARGETS[@]}"; do
|
|
if [ -d "$target" ]; then
|
|
# "! -type d" rather than "-type f": a symlinked or otherwise unusual
|
|
# artifact must produce a visible skip line, never vanish from the run.
|
|
# sort keeps the report order stable across platforms (find order is not).
|
|
while IFS= read -r f; do
|
|
[ -n "$f" ] && check_file "$f"
|
|
done <<EOF
|
|
$(find "$target" ! -type d | LC_ALL=C sort)
|
|
EOF
|
|
elif [ -f "$target" ]; then
|
|
check_file "$target"
|
|
else
|
|
echo "FAIL: $target is neither a file nor a directory" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
printf '\n'
|
|
if [ "$checked_files" -eq 0 ]; then
|
|
echo "FAIL: no ELF/Mach-O/PE binary found in the given targets ($skipped_files file(s) skipped)" >&2
|
|
echo " a gate that checked nothing is not a green gate" >&2
|
|
exit 2
|
|
fi
|
|
if [ "$fail_count" -ne 0 ]; then
|
|
echo "BINARY COMPOSITION GATE FAILED: $fail_count assertion(s) over $checked_files binary/binaries" >&2
|
|
for f in "${FAILURES[@]}"; do
|
|
echo " - $f" >&2
|
|
done
|
|
exit 1
|
|
fi
|
|
echo "BINARY COMPOSITION OK: $pass_count assertion(s) passed over $checked_files binary/binaries"
|