Skip to content

Commit 4721b58

Browse files
committed
M8: non-functional close-out audit + cheap fixes
Five-dimension audit (coverage, quality, performance, security, docs) of the post-M7 state; full report in M8-AUDIT.md. Verdict: pure CCFE::* modules are excellent across the board (Devel::Cover: 92-100%); residual debt is the oversized ccfe.pl subs and, most importantly, RESTRICTED mode being a guardrail rather than a security boundary. Cheap fixes applied (all 313 tests green): - code: 3 string-vs-numeric operator mismatches on counts ($c eq 0, $nfound eq/ge 2 -> ==/>=); removed 2 dead lexicals ($labelLen, $sc); ralign() eval-string -> sprintf (also closes a latent injection smell). - security/docs: documented RESTRICTED's true scope in the README (it read stronger than the code delivers). - consistency: version 2.1.1 across README/RPM/Alpine/packaging-README; perl >= 5.36 floor on RPM + Alpine (matching use v5.36 / debian); fixed README man-page names (ccfe_menu/ccfe_form) and a stale backup caveat; resolved the ROADMAP / M7-CTX-PLAN "M7 in progress vs complete" contradiction. Filed (in M8-AUDIT.md / FEATURE-REQUESTS): RESTRICTED hardening (locked config, read-only objects, shell-free argv exec, eval-free colour parsing); browser/exec/shell-escape test coverage; do_form-led sub refactor + man-page rewrite; man pages onto MANPATH + module POD. None block a 2.2 release. ROADMAP: M8 done -> all milestones M0-M8 delivered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2d6dabd commit 4721b58

8 files changed

Lines changed: 239 additions & 21 deletions

File tree

M7-CTX-PLAN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# M7 — `$ctx` threading plan (de-globalisation finish)
22

3+
> **STATUS: COMPLETE.** All phases 0–6 below are done; `ccfe.pl` runs under
4+
> `use v5.36`. This document is kept as the record of how the de-globalisation
5+
> was carried out. The narrative below is written from the start of that work.
6+
37
The pure-parser/geometry extractions (REFACTOR.md §3.1–3.2, first half) are
48
done: `CCFE::MenuFile`, `FormFile`, `Config`, `Action`, `Layout` are split out,
5-
unit-tested, and released in 2.1.1. What remains of M7 is the harder half:
9+
unit-tested, and released in 2.1.1. What remained of M7 was the harder half:
610
**replace the package globals and `local` dynamic scope in `ccfe.pl` with an
711
explicit `$ctx` state object** (REFACTOR.md §3.2, bullet 3 — "the spookiest
812
action-at-a-distance in the code").

M8-AUDIT.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# M8 — Non-functional close-out audit
2+
3+
Five-dimension review of CCFE at the post-M7 state (v2.1.1, `use v5.36`, 313
4+
tests). Each dimension below gives a verdict, the findings, and whether each was
5+
**fixed in this audit** or **filed** for later. Cheap, safe fixes were applied
6+
now; structural and design-level items are filed (tracked in
7+
`FEATURE-REQUESTS.md`).
8+
9+
Overall: the codebase is in good shape. The pure `CCFE::*` modules are
10+
excellent across every dimension; the residual debt is concentrated in the
11+
legacy `ccfe.pl` body (size/complexity) and in the **security model of
12+
RESTRICTED mode**, which is the one finding that materially changes how CCFE
13+
should be described and deployed.
14+
15+
---
16+
17+
## 1. Test coverage
18+
19+
**Verdict: strong where it's been invested (modules), with real gaps in the
20+
`ccfe.pl` interior.** Measured with `Devel::Cover` (require-based + module
21+
tests; the pty tests exec a separate, uninstrumented process, so true `ccfe.pl`
22+
coverage is higher than the raw number):
23+
24+
| Unit | stmt | branch | sub | total |
25+
|------|------|--------|-----|-------|
26+
| Action / Config / Context | 100 | 100 | 100 | **100** |
27+
| MenuFile | 100 | 100 | 100 | 97.3 |
28+
| Restrict | 100 | 100 | 100 | 97.0 |
29+
| Layout | 100 | 87.5 | 100 | 96.6 |
30+
| FormFile | 98.9 | 93.7 | 100 | 92.7 |
31+
| Theme | 70.6 | 75.0 | 80 | 70.1 |
32+
33+
Findings:
34+
- **[med] Output-browser interior untested**`run_browse`'s `/`+`n` search
35+
(`get_search_buff`/`search_next`/`search_all`) and the save-to-file /
36+
save-to-runnable-script paths never run. The pty harness makes these
37+
testable. *Filed.*
38+
- **[med] Error/failure branches untested**`exec_command` command-not-found,
39+
the silent `list_cmd` failure behind issue #1 (guarded only by source-pattern
40+
match in `t/02`), file-open errors in save. The pty harness those tests asked
41+
for now exists. *Filed.*
42+
- **[med] F7 shell-escape runtime path** — denial is unit-tested; the allowed
43+
spawn-and-return is not. *Filed.*
44+
- **[low] `Theme.pm` at 70 %** — the colour-pair init paths need a live
45+
terminal; could be covered with a small `Curses` mock or a pty assertion.
46+
*Filed.*
47+
- **[low] CLI: `-s`/`list_shortcuts`, `--help`/`--version`** untested (vs
48+
`-k`/`--dump`/`--plugins` which are). Trivial to add to `t/07`. *Filed.*
49+
50+
## 2. Code quality
51+
52+
**Verdict: modules pristine; the debt is all in `ccfe.pl`, correctly kept out
53+
of the CI perlcritic gate.** `perlcritic src/lib` is clean at severity 3.
54+
`ccfe.pl` shows 107 sev-5 / 441 sev-3 — but the bulk is intentional legacy idiom
55+
(bareword filehandles, stringy `eval` for colour config, non-lexical loop
56+
iterators) that would be churn-for-churn to "fix".
57+
58+
**Fixed now:**
59+
- 3 string-vs-numeric operator mismatches on counts (`$c eq 0`, `$nfound eq 2`,
60+
`$nfound ge 2``==`/`>=`).
61+
- 2 confirmed-unused lexicals removed (`$labelLen`, `$sc` — the latter left over
62+
from the FormFile extraction).
63+
- `ralign()` rewritten from an `eval`-string to a plain `sprintf` (also a
64+
security fix — see §4).
65+
- Version strings reconciled to 2.1.1 (README, RPM spec, Alpine APKBUILD,
66+
packaging README) and the RPM/Alpine perl floor set to `>= 5.36`.
67+
68+
**Filed:**
69+
- **Six oversized subs** dominate the complexity — `do_form` (1392 lines,
70+
cyclomatic 285), `load_config` (582), `run_browse` (543), `do_list` (350),
71+
`do_menu` (331), `load_form` (224). `do_form` is the clear refactor target.
72+
- The colour/attribute config dispatch is a long copy-pasted `if/elsif` of
73+
`eval "$ctx->{cfg}{X} = …"` arms — table-driveable (and see §4 for the eval).
74+
- `ccfe.pl` is ~817 lines off perltidy; reformatting is high-churn and best done
75+
alongside breaking up the big subs, so it stays ungated for now.
76+
77+
## 3. Performance
78+
79+
**Verdict: sound for an interactive TUI — no must-fix.** Scrolling uses a
80+
pre-built `curses` pad with O(1) `prefresh`; menu/form navigation delegates to
81+
the ncurses `menu_driver`/`form_driver`; heavy window/field rebuild is correctly
82+
gated to `KEY_RESIZE`, not run per keystroke. Startup parsing is linear and
83+
one-shot.
84+
85+
Findings (all low, all filed):
86+
- The streaming output-capture loop calls `trace(…, $LOG_ACTION_OUT)` per line,
87+
and both `trace()` and the new `$SIG{__WARN__}` handler open/append/close the
88+
log **per event**. In normal use this is dormant (`$LOG_ACTION_OUT` is off at
89+
the default log level; warnings are rare on hot paths), but a user who raises
90+
the log level gets per-line file I/O while a command streams. Cheap future
91+
fix: open the log once around the capture loop / hold a handle.
92+
- `trace()` builds `@months` + `localtime` before checking whether logging is
93+
even on — trivial, move the gate first.
94+
- `/`-search reads each pad cell with a char-at-a-time `inchnstr`; one-shot per
95+
search, bounded by `MAX_PAD_LINES`. Could read a row per call. Low.
96+
97+
## 4. Security
98+
99+
**Verdict: RESTRICTED mode is a *guardrail*, not a security boundary.** It
100+
reliably blocks the casual interactive escapes it was built for (F7 shell,
101+
save-as-runnable-script, `system:`/`exec:` of non-allowlisted program names),
102+
and the supporting hygiene is sound — `harden_child_env` strips `LD_*`/`BASH_ENV`
103+
/`ENV`/`CDPATH` and resets `IFS`; temp files use `O_EXCL`; persistent files are
104+
`0600` under the user's own dir; the log is `umask 0177`. But it is **not** a
105+
boundary against a motivated local user.
106+
107+
**Fixed now:**
108+
- `ralign()` `eval`-string → `sprintf` (removed a latent Perl-injection smell on
109+
boolean-field defaults).
110+
- Documented the real scope of RESTRICTED in the README (it previously read as a
111+
stronger guarantee than the code provides).
112+
113+
**Filed (the RESTRICTED hardening — needs design, see FEATURE-REQUESTS):**
114+
- **[high] User config can switch RESTRICTED off.** `load_config` applies every
115+
file on `@cnf_path` in order (system → `~/.config/ccfe` → legacy) with
116+
last-wins and no "system locks the value", and the search paths honour
117+
user-controlled `CCFE_*_DIR`/`XDG_*`. A user re-sets `restricted = no`. Fix
118+
direction: a locked/system-config mode that user config cannot weaken.
119+
- **[high] Menus/forms load from user-writable dirs.** `@mf_path` includes
120+
`~/.local/share/ccfe` and honours `CCFE_OBJ_DIR`/`-l`. A user authors any
121+
`run:` action (never allowlisted) → arbitrary command execution. Fix
122+
direction: refuse user-writable object/config dirs in restricted deployments.
123+
- **[high] Allowlist is bypassable by shell chaining.** Actions run via
124+
`/bin/sh -c`, but the allowlist only checks the first token's basename, so
125+
`system: df; sh`, `df $(sh)`, backticks all pass. Same for `%{FIELD}` values
126+
substituted into args before the check. Fix direction: argv-based (shell-free)
127+
exec so the allowlist actually constrains what runs.
128+
- **[med] `eval "$VAR = $attrv"` in `load_config`** executes arbitrary Perl from
129+
a config value (colour/attr settings). Trusted-config assumption, but combined
130+
with the above it's an in-process code-exec primitive. Fix direction: validate
131+
`$attrv` against the expected `A_*`/`COLOR_PAIR(n)`/`color_pair('x','y')`
132+
grammar (or parse, don't eval).
133+
- **[low] CWD (`.`) on `$PATH`** in `run_browse`; **[low]** `$prompt` in
134+
`call_shell` interpolated into a shell string. Both filed (the `.`-on-PATH
135+
change risks breaking menus that call a script in their own dir, so it needs a
136+
deliberate decision, not a drive-by edit).
137+
138+
## 5. Documentation
139+
140+
**Verdict: README prose is good and current on M6 features; the gaps are the
141+
man pages and a couple of self-contradictions, mostly cheap.**
142+
143+
**Fixed now:**
144+
- README version line 2.0 → 2.1.1; the `man ccfe-menu`/`ccfe-form` (hyphen)
145+
references corrected to the actual `ccfe_menu`/`ccfe_form` page names; the
146+
stale "a future release reorganises these paths" caveat updated (that reorg
147+
shipped in 2.0).
148+
- ROADMAP self-contradiction (the `$ctx` bullet still tagged 🔄 "in progress"
149+
while the rest says M7 complete) → ✅; `M7-CTX-PLAN.md` given a COMPLETE
150+
banner.
151+
152+
**Filed:**
153+
- **[high] Man pages are stale**`ccfe.1` etc. still say version 1.58, document
154+
the pre-v2 `/usr/lib/ccfe` layout, and omit every M6 flag (`-k`, `-D/--dump`,
155+
`-P/--plugins`) and feature (mouse, colour, resize). Needs a rewrite pass.
156+
- **[med] Deb ships man pages off the MANPATH** (`/usr/lib/ccfe/man/...`), so
157+
`man ccfe` won't resolve from a package install despite the README saying it
158+
will. Packaging fix (symlink/install into the man hierarchy).
159+
- **[med] No POD / contributor architecture doc** — the `CCFE::*` modules have
160+
good header comments but zero POD, and there's no "lib/CCFE overview" for a
161+
new contributor. Additive.
162+
- **[low] Two licence files** (`LICENCE` 2021 + `LICENSE` 2026) and a stale
163+
`doc/ChangeLog` in the staged tree — tidy up.
164+
165+
---
166+
167+
## What was fixed in this audit
168+
169+
`ccfe.pl`: 3 numeric operators, 2 dead lexicals, `ralign` eval→sprintf.
170+
Docs: README (version, man-page names, RESTRICTED scope, backup caveat),
171+
ROADMAP + M7-CTX-PLAN consistency. Packaging: version 2.1.1 across
172+
RPM/Alpine/packaging-README, perl `>= 5.36` floor on RPM/Alpine. All 313 tests
173+
stay green.
174+
175+
## What is filed (recommended next, roughly by priority)
176+
177+
1. **RESTRICTED-mode hardening** (the §4 high items) — locked system config,
178+
read-only object dirs in restricted deployments, shell-free argv exec, and
179+
`eval`-free colour-config parsing. This is the highest-value follow-up.
180+
2. **Coverage**: pty tests for the browser search/save paths, `exec_command`
181+
failure (the real issue-#1 repro), and F7 shell-escape.
182+
3. **`do_form` (and the other oversized subs) refactor**, with a perltidy pass
183+
and man-page rewrite once the structure settles.
184+
4. **Packaging**: man pages onto the MANPATH; module POD + a contributor doc.
185+
186+
Performance items are genuinely low priority. None of the filed items block a
187+
2.2 release.

README.MD

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ the manual pages are under `src/man/`.
1515

1616
## Status
1717

18-
`src/ccfe.pl` is the program (currently version **2.0**). The ongoing
18+
`src/ccfe.pl` is the program (currently version **2.1.1**). The ongoing
1919
modernisation — packaging, security hardening, optional colour and a move to a
2020
modular, more functional structure — is described in
2121
[`REFACTOR.md`](REFACTOR.md) and [`ROADMAP.md`](ROADMAP.md). CCFE depends on
@@ -111,6 +111,19 @@ environment variables, so menus can consume input safely (e.g.
111111
`run:grep -- "$CCFE_FIELD_PATTERN" /var/log/syslog`) instead of interpolating
112112
`%{PATTERN}` into a shell string.
113113

114+
**Scope of the guarantee.** Restricted mode is a *guardrail against wandering
115+
off the menu*, not a hardened security boundary against a determined local
116+
user. Be aware that: the allowlist matches the command's program name, but
117+
actions run through a shell, so `system: df; sh` (chaining) or a `%{FIELD}`
118+
value containing shell metacharacters can reach a shell — prefer `run:` with
119+
`"$CCFE_FIELD_*"` and avoid `%{...}` interpolation; `run:` itself is never
120+
allowlisted; and CCFE reads config and menu/form files from the user's own
121+
`~/.config/ccfe` and `~/.local/share/ccfe` (and honours `CCFE_*_DIR`/`-l`), so
122+
a user who can write those can change what runs. For a genuine kiosk boundary,
123+
deploy CCFE with system-owned, user-unwritable config and object directories
124+
(and consider the OS-level confinement you'd use for any restricted login). See
125+
`M8-AUDIT.md` for the full analysis.
126+
114127
## Colour and styles
115128

116129
CCFE is monochrome by default. When the terminal supports colour (and
@@ -212,7 +225,7 @@ item { id=NET descr=Network connections action=run:ss -tunap }
212225
```
213226

214227
then run `ccfe mytools`. See the manual pages under `src/man/` (or
215-
`man ccfe`, `man ccfe-menu`, `man ccfe-form` after install) for the full
228+
`man ccfe`, `man ccfe_menu`, `man ccfe_form` after install) for the full
216229
file-format reference, and `ccfe-plugin-sysmon` for forms and `list_cmd`
217230
dropdowns.
218231

@@ -259,8 +272,8 @@ git add . && git commit -m "My CCFE config and menus"
259272

260273
To restore on a new machine, install CCFE (or the package), then copy these
261274
files back into place. `ccfe -k <name>` parse-checks a menu/form after you
262-
restore or edit it. (A future release reorganises these paths and adds a
263-
guided config/backup workflow — see `ROADMAP.md`.)
275+
restore or edit it. (A guided config/backup workflow is a planned enhancement —
276+
see `FEATURE-REQUESTS.md`.)
264277

265278
## Tests
266279

ROADMAP.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ the manifest at install time (dependency checks, clean uninstall).
151151
landed in two halves: the pure parser/geometry extractions (`CCFE::MenuFile`/
152152
`FormFile`/`Config`/`Action`/`Layout`, released in 2.1.1) and the `$ctx`
153153
threading (phases 0–6, see **M7-CTX-PLAN.md**), finishing with `use v5.36` on
154-
`ccfe.pl`. Next: **M8** (the non-functional close-out audit).
154+
`ccfe.pl`. **M8** (the non-functional close-out audit) is also complete — see
155+
`M8-AUDIT.md`. **All ROADMAP milestones M0–M8 are now delivered.**
155156

156157
## M7 — De-globalisation / full modularisation _(REFACTOR §3 — deferred to end)_
157158
Extract `MenuFile`/`FormFile`/`Action`/`Layout`/`Exec`/`UI::*` into
@@ -204,7 +205,7 @@ the conformance tests.
204205
`new_field`/`move_field` calls and tracing stay in `ccfe.pl`. Unit-tested with
205206
hand-computed expectations in `t/17-layout.t` (27 cases); the resize/layout/
206207
multipage tty tests and full suite (302 tests) stay green.
207-
- 🔄 **`$ctx` threading** (in progress): replacing `ccfe.pl`'s package globals
208+
- **`$ctx` threading** (done): replacing `ccfe.pl`'s package globals
208209
and `local` dynamic scope with one explicit run-state object. Planned in
209210
**M7-CTX-PLAN.md** (phases 0–6, all in scope). **Phase 0 done:**
210211
`CCFE::Context::new()` container built at startup, `t/18-context.t`, and the
@@ -235,9 +236,15 @@ the conformance tests.
235236
undef trace level, undef boolean descriptions headless, negative rule-line
236237
repeat). 313 tests green, zero runtime warnings; CI's `perl -c` enforces it.
237238

238-
## M8 — Non-functional close-out audit _(final gate)_
239+
## M8 — Non-functional close-out audit _(final gate)_**done**
239240
Five dimensions: **test coverage, code quality, performance, security,
240-
documentation**. Produce a short report, fix what's cheap, file the rest.
241+
documentation**. Report in **`M8-AUDIT.md`**; cheap fixes applied (numeric
242+
operators, dead lexicals, `ralign` eval→sprintf, version sync, README/ROADMAP
243+
consistency, RESTRICTED-scope doc), the rest filed. Headline finding: the pure
244+
modules are excellent on every dimension; the residual debt is the oversized
245+
`ccfe.pl` subs and — most importantly — **RESTRICTED mode is a guardrail, not a
246+
security boundary** (user-writable config/objects + shell-based exec defeat it);
247+
hardening it is the top filed follow-up. No filed item blocks a 2.2 release.
241248

242249
---
243250

packaging/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Runtime dependencies are only **perl** and the **Curses** module
1414
Built and tested in this repo:
1515

1616
```sh
17-
dpkg-buildpackage -b -us -uc # produces ../ccfe_2.0_all.deb
18-
sudo apt install ../ccfe_2.0_all.deb
17+
dpkg-buildpackage -b -us -uc # produces ../ccfe_2.1.1_all.deb
18+
sudo apt install ../ccfe_2.1.1_all.deb
1919
```
2020

2121
(`build-essential` is not actually needed — CCFE compiles nothing — so add

packaging/alpine/APKBUILD

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
#
1010
# NOTE: not built/tested in this environment (no abuild available here).
1111
pkgname=ccfe
12-
pkgver=2.0
12+
pkgver=2.1.1
1313
pkgrel=0
1414
pkgdesc="Curses Command Front-end"
1515
url="https://github.com/OpusVL/perl-ccfe"
1616
arch="noarch"
1717
license="GPL-2.0-or-later"
18-
depends="perl perl-curses"
18+
depends="perl>=5.36 perl-curses"
1919
makedepends="perl"
2020
source="$pkgname-$pkgver.tar.gz"
2121
builddir="$srcdir/$pkgname-$pkgver"

packaging/rpm/ccfe.spec

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
# NOTE: not built/tested in this environment (no rpmbuild available here).
1111

1212
Name: ccfe
13-
Version: 2.0
13+
Version: 2.1.1
1414
Release: 1%{?dist}
1515
Summary: Curses Command Front-end
1616
License: GPLv2+
1717
URL: https://github.com/OpusVL/perl-ccfe
1818
Source0: %{name}-%{version}.tar.gz
1919
BuildArch: noarch
2020
BuildRequires: perl
21-
Requires: perl
21+
Requires: perl >= 5.36.0
2222
Requires: perl-Curses
2323

2424
%description
@@ -46,6 +46,11 @@ ln -sf ../lib/ccfe/bin/ccfe %{buildroot}%{_bindir}/ccfe
4646
%doc README.MD
4747

4848
%changelog
49+
* Wed Jun 11 2026 CCFE maintainers <ccfedevel@gmail.com> - 2.1.1-1
50+
- CCFE 2.1.1: terminal-resize reflow, display-column (wide-char) layout, full
51+
colour palette + panel theme, opt-in mouse, --dump/--plugins, and an internal
52+
de-globalisation onto pure CCFE::* modules (requires Perl >= 5.36).
53+
4954
* Tue Jun 10 2026 CCFE maintainers <ccfedevel@gmail.com> - 2.0-1
5055
- CCFE 2.0: reorganised layout, runtime path resolution, restricted mode,
5156
optional colour with SMIT themes, and the -k linter.

0 commit comments

Comments
 (0)