Skip to content

Commit 65f0912

Browse files
fix(scanner): rebuild stale escript to prevent silent false negatives (#278)
## Soundness audit finding `hypatia-cli.sh` rebuilt the escript only when it was **missing**, never when it was **older than the rule sources** (`[[ ! -x "${ESCRIPT}" ]]`). ### Adversarial reproduction A locally-built `hypatia` escript dated 2026-05-14 (3 days behind `lib/rules/code_safety.ex` @ 2026-05-17) was run against known-bad samples: | Sample | Result | |---|---| | Idris2 `believe_me` | caught (critical) | | Rust `.unwrap()` | caught (high) | | `curl \| bash` | caught (high) | | **Elixir `System.shell("echo #{user}")`** | **0 findings (false negative)** | `CodeSafety.scan_content(content, "elixir")` flags that sink **critical** when called directly, and the freshly-rebuilt escript catches all five. The detection engine and the Idris2/Lean proofs are sound — the deployment wrapper was serving a stale artifact that silently dropped the entire Elixir/Erlang/Coq/Lean/Agda/Zig/F\*/Ada pattern families. ### Fix Add `escript_is_stale()`: rebuild when the binary is missing **or** older than any `lib/**.ex(s)` / `mix.exs`. `HYPATIA_NO_STALE_REBUILD=1` opts out for air-gapped deploys; warns (does not silently proceed) when stale and `mix` is unavailable. Logic unit-tested across missing/fresh/stale/opt-out cases. Refs hyperpolymath/standards#124 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8f16d16 commit 65f0912

1 file changed

Lines changed: 27 additions & 0 deletions

File tree

hypatia-cli.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,40 @@ build_escript() {
4444
)
4545
}
4646

47+
# A stale escript is a SOUNDNESS hazard, not a convenience issue: an
48+
# escript built before a rule/pattern was added silently emits zero
49+
# findings for that whole pattern family (observed: an escript predating
50+
# the Elixir/Erlang/Coq/Lean/Agda/Zig pattern sets passed a textbook
51+
# `System.shell("…#{x}")` injection with "0 findings"). So rebuild when
52+
# the binary is missing *or* older than any tracked rule/CLI source.
53+
# Set HYPATIA_NO_STALE_REBUILD=1 to opt out (e.g. air-gapped deploys
54+
# that ship a known-current binary and have no toolchain).
55+
escript_is_stale() {
56+
[[ -x "${ESCRIPT}" ]] || return 0
57+
[[ "${HYPATIA_NO_STALE_REBUILD:-0}" == "1" ]] && return 1
58+
local newer
59+
newer=$(find "${HYPATIA_DIR}/lib" "${HYPATIA_DIR}/mix.exs" \
60+
-name '*.ex' -o -name '*.exs' 2>/dev/null \
61+
| while IFS= read -r f; do
62+
[[ "$f" -nt "${ESCRIPT}" ]] && { echo stale; break; }
63+
done)
64+
[[ -n "$newer" ]]
65+
}
66+
4767
# ─── Locate or build the escript ────────────────────────────────────────
4868
4969
if [[ ! -x "${ESCRIPT}" ]]; then
5070
# Try to build if mix is available
5171
if command -v mix &>/dev/null; then
5272
build_escript
5373
fi
74+
elif escript_is_stale; then
75+
if command -v mix &>/dev/null; then
76+
echo "[hypatia] escript is older than rule sources — rebuilding to avoid silent false negatives." >&2
77+
build_escript
78+
else
79+
echo "[hypatia] WARNING: escript is older than rule sources and mix is unavailable; scan results may omit newer pattern families." >&2
80+
fi
5481
fi
5582
5683
# ─── Forward arguments ─────────────────────────────────────────────────

0 commit comments

Comments
 (0)