Skip to content

docs(security): name every dotfile directory in the audit scopes, and dormouse-lib as a root - #449

Closed
dormouse-bot wants to merge 3 commits into
mainfrom
docs/security-scope-enumerations
Closed

docs(security): name every dotfile directory in the audit scopes, and dormouse-lib as a root#449
dormouse-bot wants to merge 3 commits into
mainfrom
docs/security-scope-enumerations

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

The 2026-08-26T18:10Z audit run on #447 surfaced two places where SECURITY.md disagrees with what it describes. Neither is a live exposure, and neither is the reason that run reported FAIL — but both would be re-reported by every future run, so they are worth closing. Review of the first fix turned up a third instance of the same shape, fixed here too.

.vscode/ and .impeccable/ were in no domain's qualitative scope

The FAIL IF guarding this is explicit that "anything no domain names is now nobody's job", and it lists .claude/ among the paths the first version of the split silently orphaned. The same shape recurred: application-security's "the repository root files" catch-all reads as covering dotfile directories without ever naming them, and the audit flagged the ambiguity as a WARNING after reading both and finding nothing security-relevant in either.

Split on what the directory actually is, rather than on it being a dotfile:

  • .vscode/ci-and-secrets, next to .claude/. tasks.json and launch.json name shell commands a maintainer's machine runs on a keystroke, which is the same reason .claude/ sits there.
  • .impeccable/application-security. design.json is the design-token snapshot behind DESIGN.md, so it belongs with docs/ and lib/.

The edit lands in both audit subagent prompts, not only SECURITY.md. The FAIL IF checks "the union of the subagents' qualitative scopes", so .github/audit/ci-and-secrets.md and .github/audit/application-security.md are what it actually reads — SECURITY.md's bulleted division is a restatement, and editing only it would leave the check seeing the same gap.

I also added a sentence to that FAIL IF saying every dotfile directory is listed explicitly and a new one has to be added there rather than left to a catch-all, since this is the second time the catch-all has been read as coverage.

The supply-chain prose named five roots where the code has six

productDependencyFilters in website/scripts/generate-deps.js has six entries and includes dormouse-lib. The FAIL IF right below says "today the six named above". The prose paragraph between them named five and described dormouse-lib as reached "as a workspace edge".

The code is right, and the reason is worth stating rather than just correcting the count: vscode-ext has no dormouse-lib dependency at all — it imports the lib through relative paths into ../lib/src/ (15 files under vscode-ext/src/), so the extension's dependency walk never reaches it. Only dormouse-standalone's edge would, which puts the whole disclosure of lib's subtree one refactor away from disappearing. Naming it a root is what makes that not matter.

website/ outside public/ and src/ was in no scope either

Raised by the review on this PR, and it is the same defect one level down, so it is fixed here rather than tracked: the division named website/public/ (ci-and-secrets) and website/src/ (supply-chain), which reads as covering website/ while leaving website/scripts/, package.json, vite.config.ts, react-router.config.ts, and tsconfig.json owned by nobody. That orphan set includes website/scripts/generate-deps.js — whose productDependencyFilters array the roots FAIL IF watches, while the rest of the generator went unaudited.

supply-chain now owns all of website/ except public/, which is a superset of what it already had and puts the generator with the snapshot it generates. The FAIL IF gains the general rule: where a top-level directory is split between domains, state it as the whole minus its exceptions, because naming two children makes the parent look covered.

Verification
  • productDependencyFilters read at website/scripts/generate-deps.js: ["dor", "dormouse", "dormouse-standalone", "dormouse-lib", "dormouse-sidecar", "server"] — six entries.
  • dormouse-lib as a declared dependency: present in standalone/package.json and website/package.json, absent from vscode-ext/package.json (whose dependencies are node-pty and ws only). grep -rl '\.\./lib/src' vscode-ext/src/ → 15 files.
  • Tracked top-level paths from git ls-tree --name-only HEAD cross-checked against the three scope lists; .vscode/ and .impeccable/ were the only two absent, matching the audit's WARNING.
  • website/'s tracked top-level entries are public, src, scripts, package.json, vite.config.ts, react-router.config.ts, tsconfig.json — the last five were the orphan set.
  • .vscode/ contents: launch.json, tasks.json (the latter runs pnpm build:vscode via "type": "shell"). .impeccable/ contents: design.json (a schemaVersion: 2 design-token document — color roles, tonal ramps).
  • node scripts/spec-lint.mjsOK (23 specs, 24 files checked). Docs-only change; no spec under docs/specs/ is touched.
  • Changing .github/audit/ puts this commit in workflow-audit.yaml's diff window by design — though it does not actually get reported today, which is fix(workflow-audit): report commits that only touch .github/audit/ #450.

Not addressed here, because neither is mine to fix: the two BLOCKERs that make #447 FAIL are default_workflow_permissions: write / can_approve_pull_request_reviews: true (an admin toggle) and tend-mention.yaml's verify job having no permissions: block (an upstream generator gap, regenerated on every nightly). Both are analyzed in the nightly comment on #447.

… dormouse-lib as a root

Two findings from the 2026-08-26T18:10Z security audit (#447), both cases of
SECURITY.md disagreeing with what it describes.

`.vscode/` and `.impeccable/` are top-level directories that no domain's
qualitative scope named. The FAIL IF that guards this is explicit that anything
no domain names is nobody's job, and the enumeration's "the root files"
catch-all reads as covering dotfile directories without ever naming them --
the same shape that orphaned `.claude/` the first time. `.vscode/` goes to
ci-and-secrets alongside `.claude/`, since `tasks.json` and `launch.json` name
shell commands; `.impeccable/design.json` is the design-token snapshot behind
DESIGN.md, so it goes to application-security. Both audit subagent prompts get
the same edit, since the FAIL IF checks their scopes rather than SECURITY.md's
restatement of them.

Separately, the "Dependency Supply Chain" prose named five roots and called
`dormouse-lib` a workspace edge, while its own FAIL IF says "the six named
above" and `productDependencyFilters` lists six including `dormouse-lib`.
The code is right: vscode-ext imports the lib through relative paths into
`../lib/src/` rather than declaring a dependency on it, so the extension's
dependency walk never reaches it.

Neither finding is a live exposure; both would have been re-reported by every
future audit run.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 26, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1f2f69f
Status: ✅  Deploy successful!
Preview URL: https://41f377d4.mouseterm.pages.dev
Branch Preview URL: https://docs-security-scope-enumerat.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself checks out. I verified the dormouse-lib root claim independently — vscode-ext/package.json declares only node-pty and ws, and vscode-ext/src/{session-state,remote-host,peer-link-protocol,processed-pty-streams}.ts import straight out of ../../lib/src/..., so dormouse-standalone's "dormouse-lib": "workspace:*" really is the only edge that reaches it. That also settles a live inconsistency: the productDependencyFilters FAIL IF already said "today the six named above" while the prose named five and called dormouse-lib an edge. And the five tracked top-level dotfile directories (.claude/, .config/, .github/, .impeccable/, .vscode/) are each now named by exactly one domain.

Two coverage gaps that survive this change. Neither belongs in this PR — flagging them so they don't get closed out by it.

workflow-audit.yaml silently drops the commit shape this PR is

.github/audit/ is in the commit-selection window but not in the attribution window, and the second one decides what gets reported:

  • Selection (COMMITS=$(git log ... -- .github/workflows/ .config/tend.yaml .github/audit/) includes it.
  • own_changes() pathspecs only .github/workflows/ .config/tend.yaml — in both the single-parent arm and the merge-intersection arm.
  • The report loop does FILES=$(own_changes "$sha") and continues on empty, without writing a $SKIPPED line.

So a commit touching only .github/audit/ (and SECURITY.md, i.e. exactly this one) enters $COMMITS, yields an empty FILES, and vanishes — not reported, not even listed as skipped. The block comment above the selection asserts the opposite: "anything landing here is reported on its own content, which is the intent."

It fails the other way too, for mixed commits. is_tend_regen reads git show --name-only ... -- .github/workflows/, requires every entry to match tend-*.yaml, and requires uvx tend init to leave .github/workflows/ clean — it never looks at .github/audit/. A commit that edits tend-review.yaml and an audit prompt is therefore classified "reproduces from the tend generator" and skipped, with the prompt edit riding along unexamined. That is precisely the swallow the .config/tend.yaml comment names as the thing to avoid: "Both classifiers refuse any commit that touches the config, so nothing in the widened window can be swallowed by an arm that doesn't inspect it — that pairing is the invariant, not either half." .github/audit/ got widened into the window without either half of that pairing.

The fix is both halves, mirroring how .config/tend.yaml is handled: add .github/audit/ to both own_changes() pathspecs, and add [ -z "$(git show --name-only --pretty='' "$sha" -- .github/audit/)" ] || return 1 to is_tend_regen and is_renovate_pin_bump.

website/ outside public/ and src/ is in no qualitative scope

The amended division reads website/public/ (ci-and-secrets) and website/src/ (supply-chain). That leaves website/scripts/, website/package.json, website/vite.config.ts, website/react-router.config.ts, and website/tsconfig.json named by nobody — while the FAIL IF above the division says the union must cover every top-level path.

This is the same failure mode the PR body describes for dotfile directories, one level down: website/ reads as covered because two of its children are. And the orphaned set includes website/scripts/generate-deps.js — the file the paragraph two screens earlier calls the source of the disclosed dependency roots. Its productDependencyFilters array is watched by the roots FAIL IF (inside supply-chain's ## Dependency Supply Chain scope), but the rest of the generator, generate-changelog.js, and the build config are not. Giving supply-chain website/ minus public/ would be consistent with what it already owns, and would need the same edit in .github/audit/supply-chain.md.

Verification notes
  • Top-level tracked paths from git ls-files --directory | awk -F/ '{print $1}' | sort -u; the only dotfile entries are .claude, .config, .github, .impeccable, .vscode, plus .gitattributes and .gitignore as root files. .claude-pr/ is untracked.
  • .impeccable/design.json's colorMeta keys (app-bg, app-fg, surface-raised, …) mirror DESIGN.md's frontmatter colors: keys one-for-one, so the "design-token snapshot behind DESIGN.md" gloss holds.
  • .vscode/tasks.json runs pnpm build:vscode; .vscode/launch.json invokes it as preLaunchTask — the keystroke claim is accurate.
  • Workspace-edge claim holds: serverserver-lib-common, dordor-lib-common, dormouse-lib → both.
  • .github/audit/orchestrator.md and _preamble.md carry no scope enumerations, so the two domain files plus SECURITY.md are the complete set that needed the edit.
  • Line anchors: selection window, own_changes(), is_tend_regen.

The scope division named website/public/ and website/src/, which left
website/scripts/, package.json, vite.config.ts, react-router.config.ts,
and tsconfig.json in no domain's qualitative scope — the same partial-
coverage reading error the dotfile directories hit, one level down.

State the split as a whole minus its exceptions instead of naming two
children, and record that rule in the FAIL IF.
@dormouse-bot

Copy link
Copy Markdown
Collaborator Author

Both gaps confirmed against the code. Splitting them: the website/ one is this PR's own defect, the workflow-audit.yaml one is not.

website/ outside public/ and src/ — fixed here (a3d98e10). The review flagged it as out of scope, but it is the same partial-coverage reading error this PR exists to fix, one level down, in the same two files — so leaving it would mean shipping a PR that closes the FAIL IF for dotfile directories while the next audit re-reports the identical shape for website/. supply-chain now owns all of website/ except public/, which is a superset of the website/src/ it already had and puts generate-deps.js with the snapshot it generates. The FAIL IF gains the general rule rather than a third enumeration: where a top-level directory is split between domains, state it as the whole minus its exceptions, because naming two children makes the parent look covered.

workflow-audit.yaml attribution — #450. A workflow logic change, not a scope enumeration, so it is its own PR. The diagnosis holds exactly as written, including the mixed-commit half: is_renovate_pin_bump has the same hole as is_tend_regen. Verified on 5b7ea046 — the old own_changes() pathspec returns empty for it while the selection window includes it, so this PR's own first commit is precisely the commit that vanishes. #450 adds .github/audit/ to both own_changes() arms and both classifier guards.

Verification of the mixed-commit half

Both classifiers guard the config and neither looks at the audit prompts, at is_renovate_pin_bump and is_tend_regen — so the .config/tend.yaml comment's own invariant ("nothing in the widened window can be swallowed by an arm that doesn't inspect it — that pairing is the invariant, not either half") was stated but only half-implemented for .github/audit/.

website/'s tracked top-level entries are public, src, scripts, package.json, vite.config.ts, react-router.config.ts, tsconfig.json; the last five were the orphan set.

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The website/ fix closes the gap the last review raised, and the split is now disjoint and complete — I re-walked git ls-tree --name-only HEAD against the three scope lists and every top-level path lands in exactly one domain, with website/ divided as public/ (ci-and-secrets) versus everything else (supply-chain).

One thing the new prose reintroduces at a smaller scale: the build-config parenthetical in .github/audit/supply-chain.md names package.json unqualified. A root package.json exists and is application-security's under "the repository root files", so a subagent reading its own scope list sees a path that is really someone else's — the same read-the-name-not-the-parent shape the FAIL IF sentence this PR adds is written to prevent. vite.config.ts and react-router.config.ts are website-only, and there is no root tsconfig.json, so package.json is the only collision. Suggestion inline also closes the sentence, which currently has no verb before the em-dash.

Self-authored, so this is a COMMENT rather than an approval.

Comment thread .github/audit/supply-chain.md Outdated
@nedtwigg

nedtwigg commented Aug 27, 2026

Copy link
Copy Markdown
Member

Superseded by #452, which carries this PR's substance rather than replacing it — I verified each change is present before closing.

Your dormouse-lib finding was the one I did not have, and it corrected me: I had written that it is "reached as a workspace edge". It is not. vscode-ext declares only node-pty and ws and reaches the lib through relative imports into ../lib/src/ from fifteen files, so the extension's dependency walk never arrives at it — only dormouse-standalone's edge does, which puts the disclosure of lib's whole subtree one refactor from vanishing. #452 states that reason rather than just the count.

Also folded in: website/ as a subtraction (all of it except public/) instead of two named subdirectories, which is what left the build config owned by nobody; and naming dotfile directories explicitly in both prompt files rather than leaving them to a catch-all that had twice been read as coverage.

Closing as superseded, not as wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants