diff --git a/ci/selftest.sh b/ci/selftest.sh index 98aeb5b..9b0ee7f 100755 --- a/ci/selftest.sh +++ b/ci/selftest.sh @@ -206,44 +206,77 @@ ann=$(ci_php ci/check-textdomain.php "$TEXT_DOMAIN" selftest-dotted 2>&1 \ || bad "the annotated path is not a file: '$ann'" # ------------------------------------------------------- the clone's git identity -# 🔴 Grade 2 evidence: the exact line ci/bump-tested-up-to.sh carried until -# 2026-09-25, not an arbitrary `git config`. That line writes .git/config, so -# every later commit in the clone — a release, by a person — is authored by the -# robot with no trailer. Six such commits in one sibling plugin, two in +# 🔴 Grade 2 evidence: row 1 is the exact line ci/bump-tested-up-to.sh carried +# until 2026-09-25, not an arbitrary `git config`. That line writes .git/config, +# so every later commit in the clone — a release, by a person — is authored by +# the robot with no trailer. Six such commits in one sibling plugin, two in # another. git warns about an identity it cannot FIND and never about a wrong # one, so nothing in the repository ever says so. +# +# The controls are the larger half here, and three of them were live faults in +# earlier versions of this check: `--[a-z-]+` as the scope group reads `--unset` +# as a scope and flags the REPAIR, and without a trailing-value requirement a +# `$(git config user.name)` READ is a violation. A build that fails on its own +# remedy is how a correct check gets deleted — the failure accuses the check, +# and the next person believes it. +# +# 🔴 The fixtures are assembled through $gc rather than written out, because +# THIS file is a ci/*.sh file and the check reads every one of them. Spelled in +# full, its own rows match: the first version of this block reported six false +# positives and no count, while the same rows measured standalone were 16/16 +# correct. A failing probe is evidence about the probe until you have looked at +# it — and the labels have to dodge it too, which is why they name the shape +# instead of quoting the line. Identical to the credential fixtures three +# hundred lines up, one file over. +# +# The plant goes into ci/build-zip.sh because nothing here SOURCES it: static.sh +# and this script source ci/lib.sh only. A plant in a sourced file is not inert +# text — it runs, sets the clone's identity for real, and the commit fixing the +# fault is authored by the plant. nolimit-seo lost a run to exactly that. So the +# last assertion checks the clone rather than the output. echo -echo "→ ci/static.sh must catch a ci/ script that configures the clone" +echo "→ ci/static.sh must catch a written identity and ignore reads and repairs" cd "$work/repo" cp ci/build-zip.sh "$work/build-zip.orig" -for scope in '' '--local' '--global'; do - cp "$work/build-zip.orig" ci/build-zip.sh - line="git config ${scope:+$scope }user.name \"compatibility check\"" - printf '%s\n' "$line" >> ci/build-zip.sh - # The plant must BE there before any verdict is believed. A `printf` that - # silently went somewhere else leaves the control state, and every assertion - # below then passes while proving nothing — a hollow probe wearing the - # costume of the most rigorous thing in the suite. - if [ "$(grep -cF "$line" ci/build-zip.sh)" -lt 1 ]; then - bad "could not stage the git-identity fixture for '${scope:-no flag}'" - continue - fi - id_out=$(./ci/static.sh 2>&1 || true) - if printf '%s' "$id_out" | grep -q 'sets a git identity that outlives it'; then - ok "flagged git config ${scope:-(no flag)} user.name" - else - bad "NOT FLAGGED: git config ${scope:-(no flag)} user.name — the clone keeps the robot's name" - fi -done -# The correct form must NOT fire, or the fix trips the check that exists to -# protect it and the next person deletes one of the two. -cp "$work/build-zip.orig" ci/build-zip.sh -printf '%s\n' 'git -c user.name="compatibility check" -c user.email=x commit -m y' >> ci/build-zip.sh -scoped_out=$(./ci/static.sh 2>&1 || true) -printf '%s' "$scoped_out" | grep -q 'sets a git identity that outlives it' \ - && bad "FALSE POSITIVE on 'git -c user.name=… commit' — that is the fix, not the fault" \ - || ok "ignored the scoped 'git -c … commit' form" +identity_row() { # $1 = RED|green (want), $2 = label, $3 = line to plant + cp "$work/build-zip.orig" ci/build-zip.sh + printf '%s\n' "$3" >> ci/build-zip.sh + # The plant must BE there before any verdict is believed. A printf that went + # somewhere else leaves the control state, and then every row passes while + # proving nothing — and the all-green rows are exactly the ones whose zeroes + # you want to believe. + if [ "$(grep -cF "$3" ci/build-zip.sh)" -lt 1 ]; then + bad "could not stage the git-identity fixture: $2" + return + fi + row_out=$(./ci/static.sh 2>&1 || true) + if printf '%s' "$row_out" | grep -q 'sets a git identity that outlives it'; then + got=RED + else + got=green + fi + if [ "$got" = "$1" ]; then + ok "$2 -> $got" + elif [ "$1" = RED ]; then + bad "NOT FLAGGED: $2 — the clone keeps the robot's name" + else + bad "FALSE POSITIVE: $2 — the check goes red on something it must allow" + fi +} + +gc="git config" # never spelled out below: see the note above +identity_row RED 'WRITE no flag, quoted value' "$gc user.name \"compatibility check\"" +identity_row RED 'WRITE --local scope' "$gc --local user.name \"x\"" +identity_row RED 'WRITE --global scope' "$gc --global user.email \"x@y\"" +identity_row RED 'WRITE --system scope' "$gc --system user.name \"x\"" +identity_row RED 'WRITE unquoted value' "$gc user.email noreply@localhost" +identity_row green 'REPAIR --local --unset' "$gc --local --unset user.name" +identity_row green 'REPAIR --unset, no scope' "$gc --unset user.email" +identity_row green 'READ inside a command substitution' "echo \"\$($gc user.name)\"" +identity_row green 'FIX git -c ... commit' 'git -c user.name="x" -c user.email=y commit -m z' +identity_row green 'other a non-identity key' "$gc core.autocrlf false" +identity_row green 'prose a commented-out mention' "# $gc user.name \"x\" is the fault" cp "$work/build-zip.orig" ci/build-zip.sh # And the passing line must carry a number: "every script is clean" reads the @@ -253,6 +286,15 @@ printf '%s' "$clean_out" | grep -qE "^ [0-9]+ scripts in ci/ leave the clone's && ok "the git-identity check reports how many scripts it read" \ || bad "the git-identity check printed no count — it cannot tell 11 scripts from 0" +# The fixtures are lines that SET an identity. If any of them executed rather +# than being read, this clone now has one — which would also mean every verdict +# above was measured against a tree nobody intended. +if git config --local --get-regexp '^user\.' >/dev/null 2>&1; then + bad "a git-identity fixture RAN — the clone has a local identity and the rows above are void" +else + ok "no fixture executed: the clone still has no local identity" +fi + # ------------------------------------------------------------------- plugin.conf echo echo "→ the floors must be required, not defaulted" diff --git a/ci/static.sh b/ci/static.sh index a2932b3..baf4cd3 100755 --- a/ci/static.sh +++ b/ci/static.sh @@ -322,8 +322,33 @@ ci_step "no ci/ script configures the clone it was handed" # misattributed hand commits in one sibling, two in another, including # releases. This tree was clean and one run away. # -# Any flag, not just --local: --global sets the whole machine's identity, which -# is the same fault one scope wider and reaches every repository on the box. +# The pattern has to separate a WRITE from a read and from the repair, and the +# obvious widening gets all three wrong. Measured row by row: +# +# git config user.name "compatibility check" WRITE caught +# git config --local user.name "x" WRITE caught +# git config --global user.email "x@y" WRITE caught <- --local only: missed +# git config --system user.name "x" WRITE caught +# git config --local --unset user.name REPAIR ignored <- --[a-z-]+: flagged +# git config --unset user.email REPAIR ignored <- --[a-z-]+: flagged +# echo "$(git config user.name)" READ ignored <- --[a-z-]+: flagged +# git -c user.name="x" … commit -m y FIX ignored +# git config core.autocrlf false other ignored +# git config --global core.pager cat other ignored +# # git config user.name "x" (a comment) prose ignored +# (nothing planted) clean ignored +# +# ⚠️ Do NOT widen the scope group to `--[a-z-]+`. It reads `--unset` as a scope +# and then finds `user.` right after it, so the check goes red on the very +# repair this message recommends — and a build that fails on the remedy is how +# a correct check gets deleted: the failure accuses the check, and the next +# person believes it. Three sessions reached for `--[a-z-]+` independently on +# 2026-09-25, which is why it is written here rather than re-derived. +# +# Enumerating the three real scopes is one half. The other is requiring a VALUE +# after the key — `user\.[a-zA-Z]+[[:space:]]+[^[:space:])]` — which is what +# makes a read not a violation: `$(git config user.name)` has `)` where a write +# has a value. Found by nolimit-seo, whose first version failed on it. # # The rule: a script may configure ITSELF, never the working copy it was # handed. Reported by the Nolimit Security session 2026-09-25. @@ -331,7 +356,7 @@ scripts_scanned=$(git ls-files 'ci/*.sh' | wc -l) if [ "$scripts_scanned" -lt 1 ]; then note_fail "The git-identity scan found no ci/*.sh files at all, so it proved nothing." elif git ls-files -z 'ci/*.sh' \ - | xargs -0 grep -nE '^[^#]*git config([[:space:]]+--[a-z-]+)*[[:space:]]+user\.'; then + | xargs -0 grep -nE '^[^#]*git config([[:space:]]+--(local|global|system))*[[:space:]]+user\.[a-zA-Z]+[[:space:]]+[^[:space:])]'; then note_fail "A ci/ script sets a git identity that outlives it. Scope it to the one commit with 'git -c user.name=… commit', which does not write .git/config." else echo " $scripts_scanned scripts in ci/ leave the clone's git identity alone"