Make the CI checks say how many files they actually read

Four checks in ci/ printed a reassuring line having examined nothing. Found
independently by the woo-ai-manager and woo-cache-manager sessions in their own
copies of this kit; all four reproduce here.

A check that examined nothing prints the same sentence as one that examined
everything, and only the passing side can tell them apart. That is why these
survived being verified: a pattern that is too broad fails loudly the first time
it meets real code, while one that finds nothing passes forever. So every
success line now carries a count — the old text domain line could not tell 142
from 0.

- static.sh, direct-access guard: counts what the loop examined and fails below
  MIN_SHIPPED_PHP_FILES. shipped_files() swallows a dist-files.sh failure with
  `|| true`, so an empty list is one broken .distignore away, and this is the
  check standing between a PHP file and being requested straight from a browser
  outside every capability check the code assumes has happened. The floor is
  above 1 because a pattern that wrongly excludes includes/ collapses the list
  without emptying it.

- static.sh, credential scan: counts what it read, fatal on zero. A pushed key
  is compromised even after the commit is deleted, so a silent no-op here is not
  a weak check but the absence of one wearing a green tick. The floor is 1 and
  is not configurable: this list has no filter that could go partially wrong, so
  the only way it misreports is by coming out empty.

- check-textdomain.php: three zeros, three verdicts. No PHP files at all is
  always fatal and deliberately not caller-configurable — it says the scan root
  is wrong, not that the plugin is small. Below --min is fatal. Zero calls with
  no --min is a warning, since a small plugin may legitimately have no
  translatable strings, but it is never silent.

- check-textdomain.php: fix the annotated path. str_replace( $root, '', $path )
  with the default root of "." removed every dot anywhere in the path, so a real
  error in includes/class-db.php was reported on "includes/class-dbphp" — not a
  file, not clickable, and not greppable. Every error this script printed with
  the default root was mangled that way.

The floors live in ci/plugin.conf, the only file under ci/ that knows anything
about this plugin, rather than hardcoded in static.sh: static.sh is copied
verbatim between plugins too, so a number in it would make this plugin's shape
everybody's constraint. ci_load_conf now requires them — an unset floor turns
its comparison into a no-op and silently restores the bug.

Each floor was broken once and watched to fail before being believed: empty
shipped list, wrong scan root, --min above the real count, zero calls without
--min, empty credential list, and a plugin.conf missing both floors. Static
checks green, and now report 13 shipped PHP files, 142 translation calls across
17 files, 44 tracked files read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-17 00:14:40 +02:00
co-authored by Claude Opus 5
parent 7f3c8a36a2
commit 9b0b0ab475
5 changed files with 174 additions and 13 deletions
+36 -3
View File
@@ -54,12 +54,24 @@ before=$fails
# Without the guard a PHP file can be requested straight from the browser,
# outside WordPress and therefore outside every capability check the code
# assumes has already happened.
guard_scanned=0
while read -r f; do
[ -n "$f" ] || continue
guard_scanned=$((guard_scanned + 1))
grep -q "defined( 'ABSPATH' )\|defined( 'WP_UNINSTALL_PLUGIN' )" "$f" \
|| { ci_annotate error "$f" 1 "Missing the guard against direct access: if ( ! defined( 'ABSPATH' ) ) { exit; }"; fails=$((fails + 1)); }
done <<< "$(shipped_files)"
[ "$fails" = "$before" ] && echo " every shipped file is guarded"
# 🔴 The count is the result, not decoration. "every shipped file is guarded"
# is also what ZERO shipped files prints, and shipped_files() swallows a
# dist-files.sh failure with `|| true`, so an empty list is one broken
# .distignore away. The floor is above 1 on purpose: a pattern that wrongly
# excludes includes/ leaves the main file behind and collapses the list
# without emptying it. Proven by stubbing ci/dist-files.sh to print nothing.
if [ "$guard_scanned" -lt "$MIN_SHIPPED_PHP_FILES" ]; then
note_fail "Only $guard_scanned shipped PHP file(s) were examined for the direct-access guard, expected at least $MIN_SHIPPED_PHP_FILES. The file list is wrong, so this check proved nothing."
elif [ "$fails" = "$before" ]; then
echo " $guard_scanned shipped PHP files examined, every one guarded"
fi
ci_step "no debug leftovers"
if git ls-files -z '*.php' | xargs -0 grep -nE '\b(var_dump|print_r|dd)[[:space:]]*\(' ; then
@@ -86,7 +98,12 @@ done < <(git ls-files -z '*.php')
[ "$fails" = "$before" ] && echo " clean"
ci_step "text domain"
ci_php ci/check-textdomain.php "$TEXT_DOMAIN" . || fails=$((fails + 1))
# --min is deliberately far below the real count. It is not an assertion about
# how many strings this plugin has — a tight number goes red on ordinary churn
# and gets raised until it means nothing. It asserts the scan still finds this
# plugin's strings AT ALL, because "every translation call carries the text
# domain" is also what this prints having found none.
ci_php ci/check-textdomain.php "--min=$MIN_TRANSLATION_CALLS" "$TEXT_DOMAIN" . || fails=$((fails + 1))
ci_step "every file the plugin loads is a file the release contains"
# The bug this exists for: a new class file is written, required from the main
@@ -151,14 +168,30 @@ ci_step "no secrets in tracked files"
# Deliberately narrow. Broad entropy heuristics fire on minified assets, hashes
# and base64 images, and a check that cries wolf gets commented out — which is
# worse than not having it. These are prefixes that mean one thing only.
#
# 🔴 And it says how many files it read. A pushed key is compromised even after
# the commit is deleted, so this scan silently examining nothing is not a weak
# check — it is the absence of one, wearing a green tick.
#
# The floor here is 1 and is NOT plugin-configurable, unlike the shipped-file
# one above: this list is every tracked file with no filter that could go
# partially wrong, so the only way it misreports is by coming out empty, and
# any higher number would just be a guess about the plugin's size. The count on
# the success line is what catches a list that shrank without emptying.
before=$fails
secrets_scanned=0
while IFS= read -r -d '' f; do
[ -f "$f" ] || continue
secrets_scanned=$((secrets_scanned + 1))
if LC_ALL=C grep -nE '(sk-ant-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{32,}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{50,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)' "$f"; then
note_fail "$f looks like it contains a credential. It cannot be unpushed — rotate it."
fi
done < <(git ls-files -z)
[ "$fails" = "$before" ] && echo " nothing that looks like a key or a private key"
if [ "$secrets_scanned" -lt 1 ]; then
note_fail "The credential scan examined no files at all. git tracks nothing here, so this check proved nothing."
elif [ "$fails" = "$before" ]; then
echo " $secrets_scanned tracked files read, nothing that looks like a key or a private key"
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