diff --git a/ci/bump-tested-up-to.sh b/ci/bump-tested-up-to.sh index f9f3365..7480083 100755 --- a/ci/bump-tested-up-to.sh +++ b/ci/bump-tested-up-to.sh @@ -60,10 +60,15 @@ done git diff --quiet && { echo "Nothing actually changed on disk — check the sed patterns." >&2; exit 1; } branch="compat/tested-up-to-$current_wp-wc-$current_wc" -git config user.name "compatibility check" -git config user.email "noreply@localhost" git checkout -b "$branch" -git commit -am "Tested against WordPress $current_wp and WooCommerce $current_wc +# The identity is scoped to THIS commit with `git -c`, never written to the +# clone. `git config user.name` persists in .git/config, so every later commit +# in this working copy — a release days afterwards, by a person — would be +# authored by the robot with no Co-Authored-By trailer. git announces a +# MISSING identity and says nothing about a wrong one, so that is silent. +# Signing this one commit as the robot is right: it is not a person's. +git -c user.name="compatibility check" -c user.email="noreply@localhost" \ + commit -am "Tested against WordPress $current_wp and WooCommerce $current_wc $(printf '%s\n' "${changed[@]}") diff --git a/ci/selftest.sh b/ci/selftest.sh index 7b02117..98aeb5b 100755 --- a/ci/selftest.sh +++ b/ci/selftest.sh @@ -205,6 +205,54 @@ ann=$(ci_php ci/check-textdomain.php "$TEXT_DOMAIN" selftest-dotted 2>&1 \ && ok "the annotated path exists on disk: $ann" \ || 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 +# another. git warns about an identity it cannot FIND and never about a wrong +# one, so nothing in the repository ever says so. +echo +echo "→ ci/static.sh must catch a ci/ script that configures the clone" +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" +cp "$work/build-zip.orig" ci/build-zip.sh + +# And the passing line must carry a number: "every script is clean" reads the +# same over zero files as over eleven. +clean_out=$(./ci/static.sh 2>&1 || true) +printf '%s' "$clean_out" | grep -qE "^ [0-9]+ scripts in ci/ leave the clone's git identity alone" \ + && 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" + # ------------------------------------------------------------------- plugin.conf echo echo "→ the floors must be required, not defaulted" diff --git a/ci/static.sh b/ci/static.sh index 772edfa..a2932b3 100755 --- a/ci/static.sh +++ b/ci/static.sh @@ -307,6 +307,36 @@ elif [ "$fails" = "$before" ]; then echo " $secrets_scanned tracked files read, nothing that looks like a key or a private key" fi +ci_step "no ci/ script configures the clone it was handed" +# `git config user.name` writes .git/config and OUTLIVES the script. A helper +# that sets an identity before committing therefore renames every later commit +# in that working copy, including a release made by a person days afterwards +# and with no Co-Authored-By trailer. git announces an identity it cannot FIND +# and says nothing about one that is wrong, so the missing case is loud and +# this one is silent: nothing fails, nothing warns, and the author is only +# visible to somebody reading `git log` for another reason. +# +# ci/bump-tested-up-to.sh did this until it was scoped to `git -c … commit`, +# which still signs its own commit as the robot — correct, that one is not a +# person's — without touching the clone. Measured across the family: six +# 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 rule: a script may configure ITSELF, never the working copy it was +# handed. Reported by the Nolimit Security session 2026-09-25. +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 + 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" +fi + ci_step "no placeholders left in the plugin header" # The header is the plugin's business card: wp-admin shows the author and the # links to whoever installs it, so a placeholder there survives for as long as