From 20f045134452d5a711fc40971ea1e64006f2cc74 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 29 Aug 2026 06:56:09 -0500 Subject: [PATCH 1/2] Add a gate that keeps FOS prose in US spellings The sweep in #168 made the tree clean; nothing stopped it drifting back. This is the gate. It matters more here than in most repos because these strings are read off a screen by a technician standing at a machine mid-enrollment. A whole-tree scan, not a diff. The obvious shape is "check only the lines this pull request adds", and it needs a base ref: a shallow clone has no merge-base, so the check would find nothing to look at and pass -- for a reason that has nothing to do with spelling, silently and forever. A gate that can only report success is worse than no gate, because it also reports "verified". Scanning everything needs no ref, cannot skip, and is only possible because the tree is already clean. Two patterns, because one right-hand boundary cannot serve both shapes. The word/camelCase one refuses a following lower-case letter, so `enrol` does not fire inside `enrolled` or `enrollment`. The ALL CAPS one refuses a following letter of any case, or `ENROL` matches inside `ENROLL_SECUREBOOT` and the gate fails on a correctly spelled name. Both are case-sensitive: a trailing /i also folds the [a-z] and [A-Z] in the boundary assertions, which collapses the camelCase hump into "letter, letter" -- no boundary at all. Also sweeps the five words the first pass missed (afterwards, towards, artefacts) now that the sweep carries the match's own case rather than an enumerated list of forms. Proven by mutation, not by being green: a lower-case word, a Title-case word, an ALL-CAPS word, an ALL-CAPS word inside SNAKE_CASE, a camelCase identifier, a UK word in an untracked new file, and a broken file enumeration each make it fail; `enrollment`, `enrolled` and `ENROLL_SECUREBOOT` each leave it green. tests/run-all.sh: 18 passed, 0 failed. Co-Authored-By: Claude --- .../rootfs_overlay/usr/share/fog/lib/funcs.sh | 2 +- docs/adr/0009-secure-boot-enrolment-paths.md | 6 +- tests/README.md | 8 + tests/checks/us-spelling.sh | 146 ++++++++++++++++++ tests/checks/wipe.sh | 2 +- 5 files changed, 159 insertions(+), 5 deletions(-) create mode 100755 tests/checks/us-spelling.sh diff --git a/Buildroot/board/FOG/FOS/rootfs_overlay/usr/share/fog/lib/funcs.sh b/Buildroot/board/FOG/FOS/rootfs_overlay/usr/share/fog/lib/funcs.sh index 609a9e2..5ec5636 100644 --- a/Buildroot/board/FOG/FOS/rootfs_overlay/usr/share/fog/lib/funcs.sh +++ b/Buildroot/board/FOG/FOS/rootfs_overlay/usr/share/fog/lib/funcs.sh @@ -2532,7 +2532,7 @@ wipeDisk() { echo " *** WARNING: $disk is a solid-state device (class: $class) ***" echo " Overwriting an SSD is NOT a guaranteed erase: wear levelling and" echo " over-provisioning keep copies of your data in blocks that no" - echo " overwrite can address. Data may remain recoverable afterwards." + echo " overwrite can address. Data may remain recoverable afterward." echo " Use the drive's own secure erase (ATA sanitize/secure erase) for a" echo " guaranteed wipe of this device." echo "" diff --git a/docs/adr/0009-secure-boot-enrolment-paths.md b/docs/adr/0009-secure-boot-enrolment-paths.md index 3a4b6dc..276fb8c 100644 --- a/docs/adr/0009-secure-boot-enrolment-paths.md +++ b/docs/adr/0009-secure-boot-enrolment-paths.md @@ -99,7 +99,7 @@ Could not boot: Error 0x7f04819a ``` **iPXE verifies the kernel *and* the initrd through shim.** The signed chain -loads fine right up to the point where FOG's own artefacts are checked against a +loads fine right up to the point where FOG's own artifacts are checked against a MokList that does not yet contain FOG's certificate, and both are refused. So FOS can never be the thing that establishes trust in FOG's key on a machine @@ -215,7 +215,7 @@ one. Its honest scope is: **machines that currently have Secure Boot off and are going to have it turned on.** That is a real and common case — plenty of sites disable Secure Boot precisely so -they can use FOG, and want it back on afterwards. There, the task stages the key +they can use FOG, and want it back on afterward. There, the task stages the key with no USB media, no live image and no fingerprint transcription, and the tech confirms once at MokManager. That is a genuine improvement on the USB kit for that case, and nothing more. @@ -344,7 +344,7 @@ it current. **Path 1 validated end to end on 2026-08-03** (VirtualBox 7.2 EFI, platform keys cleared to enter Setup Mode). The task enrolled with nobody at the keyboard, and -the firmware afterwards held exactly what it should: +the firmware afterward held exactly what it should: - `db` — Microsoft's five db CAs plus FOG's signing certificate - `KEK` — Microsoft's two KEK CAs plus this server's KEK diff --git a/tests/README.md b/tests/README.md index 0973b7f..f521e01 100644 --- a/tests/README.md +++ b/tests/README.md @@ -192,6 +192,14 @@ tests/checks/package-mirrors.sh # drops the superseded lines, a failed download # restores the .mk rather than leaving a half-bump, # and --dry-run writes nothing +tests/checks/us-spelling.sh # FOS's own source carries US spellings. A + # whole-tree scan rather than a diff, because a + # diff-scoped check needs a base ref a shallow + # clone does not have -- and would then pass for a + # reason unrelated to spelling, silently. Two + # patterns: word/camelCase, and ALL CAPS with a + # tighter right edge, so the five-letter UK form + # does not fire inside ENROLL_SECUREBOOT ``` Like the golden harness, the library harnesses source a sandbox copy of the diff --git a/tests/checks/us-spelling.sh b/tests/checks/us-spelling.sh new file mode 100755 index 0000000..1d575c6 --- /dev/null +++ b/tests/checks/us-spelling.sh @@ -0,0 +1,146 @@ +#!/bin/bash +# +# Gate: FOS's own source carries US spellings. +# +# tests/checks/us-spelling.sh # exit non-zero on any UK spelling in scope +# +# Tom asked for US spellings throughout. The tree was swept once (#168); this is +# what stops it drifting back, one echo line at a time, the way it drifted in. +# It matters more here than in most repos because these strings are read off a +# screen by a technician standing at a machine mid-enrollment. +# +# WHY A WHOLE-TREE SCAN RATHER THAN A DIFF +# +# The obvious shape is "check only the lines this pull request adds". It needs a +# base ref, and a shallow clone has no merge-base -- so the check would find +# nothing to look at and pass, for a reason that has nothing to do with +# spelling, silently and forever. A gate that can only report success is worse +# than no gate, because it also reports "verified". Scanning the whole tree +# needs no ref and cannot skip, and is only possible because the sweep already +# made the tree clean. +# +# Mirrors fogproject's tests/us-spelling.test.php, including the word list and +# the two-pattern boundary handling. Keep them in step. + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" + +# FOS's own source. Buildroot's vendored tree is out -- upstream's spellings are +# not ours to correct. +SCOPE=( + Buildroot/board/FOG/FOS/rootfs_overlay + tests + docs + README.md +) + +# Read by a machine or by another repository, so the UK spelling is load +# bearing. Blanked out of the line before the words below are looked for. +ALLOWED=( + # Three files here and one in fogproject reference this ADR by name. + '0009-secure-boot-enrolment-paths' +) + +# An explicit list, not a blanket -ise -> -ize rule: advertise, exercise, +# surprise and otherwise are -ise in both dialects. `enrolled` and `enrolling` +# are absent on purpose -- both dialects double the l there. +UK=( + enrolment enrolments enrol enrols + recognise recognised recognises recognising recognisable unrecognised + normalise normalised normalises normalising normalisation normaliser + behaviour behaviours behavioural + colour colours coloured colouring + cancelled cancelling + labelled labelling relabelled relabelling unlabelled signalling + modelled travelled + catalogue licence centre centres + neighbour neighbours neighbouring + initialise initialised initialises initialisation initialiser + authorise authorised authorisation + serialise serialised serialising + organise organised organising organisation + minimise minimised maximise maximised optimise optimised + utilise utilised utilising prioritise summarise summarised + specialise specialised + favour favours favourable + defence offence fulfil whilst + grey greyed + analyse analysed analyses analyser + afterwards towards amongst + judgement judgements ageing + artefact artefacts programme programmes +) + +# Lookbehind is required for the boundary rules below, so this needs PCRE. +# Refuses rather than degrading to a weaker pattern: a check that quietly +# matches less is the failure this file exists to prevent. +if ! echo x | grep -qP 'x' 2>/dev/null; then + printf 'FAIL: grep has no -P (PCRE) support; cannot run this check.\n' >&2 + exit 1 +fi + +# git ls-files --cached --others --exclude-standard, not a find: tracked files, +# plus new ones a developer has written but not staged, minus anything ignored. +mapfile -t files < <(git -C "$ROOT" ls-files --cached --others --exclude-standard -- "${SCOPE[@]}" 2>/dev/null) +if [[ ${#files[@]} -eq 0 ]]; then + # Loud, not skipped. + printf 'FAIL: could not enumerate files under %s.\n' "$ROOT" >&2 + exit 1 +fi + +# Two patterns, because one right-hand boundary cannot serve both shapes. +# +# WORD/camelCase -- lower or Title case. Left edge is "not preceded by a letter, +# OR on a camelCase hump". Right edge is "not followed by a lower-case letter", +# which keeps `enrol` from firing inside `enrolled` and `enrollment`. +# +# ALL CAPS -- for shell constants. Here the right edge must reject an upper-case +# letter too, or `ENROL` matches inside `ENROLL_SECUREBOOT` and the check fails +# on a correctly spelled name. `ENROLMENT_MODE` still matches: `_` is not a +# letter. +# +# Both are case-SENSITIVE. Adding -i would also fold the [a-z]/[A-Z] in the +# boundary assertions, collapsing the camelCase hump into "letter, letter" -- +# i.e. no boundary at all. +cased='' +upper='' +for w in "${UK[@]}"; do + cased="${cased}|${w^}|${w}" + upper="${upper}|${w^^}" +done +cased="${cased#|}" +upper="${upper#|}" +P_WORD="(?:(?/dev/null || continue + scanned=$((scanned + 1)) + while IFS= read -r hit; do + [[ -z $hit ]] && continue + printf '%s:%s\n' "$f" "$hit" + problems=$((problems + 1)) + done < <(sed "$sedscript" "$path" 2>/dev/null | grep -nP -o "$P_WORD|$P_CAPS" 2>/dev/null) +done + +if [[ $problems -gt 0 ]]; then + printf '\nFAIL: %d UK spelling(s) in scope.\n' "$problems" >&2 + printf 'Use the US form. If a machine reads the string and the UK spelling\n' >&2 + printf 'is load bearing, add it to ALLOWED above with the reason -- do not\n' >&2 + printf 'remove the word from UK.\n' >&2 + exit 1 +fi + +printf 'us-spelling: %d file(s) scanned, no UK spellings in scope\n' "$scanned" diff --git a/tests/checks/wipe.sh b/tests/checks/wipe.sh index c80aa95..03e8faa 100755 --- a/tests/checks/wipe.sh +++ b/tests/checks/wipe.sh @@ -239,7 +239,7 @@ run_case "nvme full, sanitize completes with forced dealloc -> ok" /dev/nvme0n1 # 8b. A confirmed sanitize must never be followed by a format. The drive is # already erased; the only reason to issue one would be a misread log. new_case; FAKE_SANICAP=2; FAKE_SSTAT_SEQ="2 2 1" -run_case "nvme full, sanitize completes -> no format afterwards" /dev/nvme0n1 full ok "" "format" +run_case "nvme full, sanitize completes -> no format afterward" /dev/nvme0n1 full ok "" "format" # 9. Unreadable sanitize log AFTER the sanitize started. The regression this # harness missed: the log parse failed on the first poll, the code called that From 012fd348aecfe77d377b750802c8ef69f58c5bd7 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 29 Aug 2026 07:04:51 -0500 Subject: [PATCH 2/2] Fail the spelling gate when a scope path matches nothing The empty-enumeration guard covered "git exited non-zero". It did not cover the case that actually happens: git exits 0 and returns no rows, because a path moved or an ignore rule grew to swallow it. That printed "0 file(s) scanned, no UK spellings in scope" and exited 0 -- green, and meaningless. Exactly the silent pass the whole file exists to avoid. Checked per SCOPE entry rather than as a total, because a total still passes when one directory of the four drops out. Mutation-proven both ways: renaming one scope entry fails, and making the enumeration return nothing at all fails. tests/run-all.sh: 18 passed, 0 failed. Co-Authored-By: Claude --- tests/checks/us-spelling.sh | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/checks/us-spelling.sh b/tests/checks/us-spelling.sh index 1d575c6..b91f64d 100755 --- a/tests/checks/us-spelling.sh +++ b/tests/checks/us-spelling.sh @@ -82,9 +82,27 @@ fi # git ls-files --cached --others --exclude-standard, not a find: tracked files, # plus new ones a developer has written but not staged, minus anything ignored. mapfile -t files < <(git -C "$ROOT" ls-files --cached --others --exclude-standard -- "${SCOPE[@]}" 2>/dev/null) -if [[ ${#files[@]} -eq 0 ]]; then - # Loud, not skipped. - printf 'FAIL: could not enumerate files under %s.\n' "$ROOT" >&2 + +# Per SCOPE entry, not just a total. git exits 0 quite happily on a checkout +# where a path has moved or where an ignore rule now swallows it, and the result +# would be "0 file(s) scanned, no UK spellings in scope" -- green, and +# meaningless. A total still passes when one directory of the four drops out, so +# the floor is per path. +empty=() +for want in "${SCOPE[@]}"; do + found=0 + for f in "${files[@]}"; do + if [[ $f == "$want" || $f == "$want"/* ]]; then + found=1 + break + fi + done + [[ $found -eq 0 ]] && empty+=("$want") +done +if [[ ${#empty[@]} -gt 0 ]]; then + printf 'FAIL: these scope paths matched no files -- moved, renamed or\n' >&2 + printf 'newly ignored? Fix SCOPE; do not delete the entry to go green.\n' >&2 + printf ' %s\n' "${empty[@]}" >&2 exit 1 fi