docs(security): name every dotfile directory in the audit scopes, and dormouse-lib as a root - #449
docs(security): name every dotfile directory in the audit scopes, and dormouse-lib as a root#449dormouse-bot wants to merge 3 commits into
Conversation
… 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.
Deploying mouseterm with
|
| Latest commit: |
1f2f69f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://41f377d4.mouseterm.pages.dev |
| Branch Preview URL: | https://docs-security-scope-enumerat.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
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")andcontinues on empty, without writing a$SKIPPEDline.
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.gitattributesand.gitignoreas root files..claude-pr/is untracked. .impeccable/design.json'scolorMetakeys (app-bg,app-fg,surface-raised, …) mirrorDESIGN.md's frontmattercolors:keys one-for-one, so the "design-token snapshot behindDESIGN.md" gloss holds..vscode/tasks.jsonrunspnpm build:vscode;.vscode/launch.jsoninvokes it aspreLaunchTask— the keystroke claim is accurate.- Workspace-edge claim holds:
server→server-lib-common,dor→dor-lib-common,dormouse-lib→ both. .github/audit/orchestrator.mdand_preamble.mdcarry no scope enumerations, so the two domain files plusSECURITY.mdare 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.
|
Both gaps confirmed against the code. Splitting them: the
Verification of the mixed-commit halfBoth classifiers guard the config and neither looks at the audit prompts, at
|
dormouse-bot
left a comment
There was a problem hiding this comment.
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.
|
Superseded by #452, which carries this PR's substance rather than replacing it — I verified each change is present before closing. Your Also folded in: Closing as superseded, not as wrong. |
The 2026-08-26T18:10Z audit run on #447 surfaced two places where
SECURITY.mddisagrees with what it describes. Neither is a live exposure, and neither is the reason that run reportedFAIL— 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 scopeThe
FAIL IFguarding 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 aWARNINGafter 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.jsonandlaunch.jsonname shell commands a maintainer's machine runs on a keystroke, which is the same reason.claude/sits there..impeccable/→application-security.design.jsonis the design-token snapshot behindDESIGN.md, so it belongs withdocs/andlib/.The edit lands in both audit subagent prompts, not only
SECURITY.md. TheFAIL IFchecks "the union of the subagents' qualitative scopes", so.github/audit/ci-and-secrets.mdand.github/audit/application-security.mdare 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 IFsaying 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
productDependencyFiltersinwebsite/scripts/generate-deps.jshas six entries and includesdormouse-lib. TheFAIL IFright below says "today the six named above". The prose paragraph between them named five and describeddormouse-libas reached "as a workspace edge".The code is right, and the reason is worth stating rather than just correcting the count:
vscode-exthas nodormouse-libdependency at all — it imports the lib through relative paths into../lib/src/(15 files undervscode-ext/src/), so the extension's dependency walk never reaches it. Onlydormouse-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/outsidepublic/andsrc/was in no scope eitherRaised 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) andwebsite/src/(supply-chain), which reads as coveringwebsite/while leavingwebsite/scripts/,package.json,vite.config.ts,react-router.config.ts, andtsconfig.jsonowned by nobody. That orphan set includeswebsite/scripts/generate-deps.js— whoseproductDependencyFiltersarray the rootsFAIL IFwatches, while the rest of the generator went unaudited.supply-chainnow owns all ofwebsite/exceptpublic/, which is a superset of what it already had and puts the generator with the snapshot it generates. TheFAIL IFgains 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
productDependencyFiltersread atwebsite/scripts/generate-deps.js:["dor", "dormouse", "dormouse-standalone", "dormouse-lib", "dormouse-sidecar", "server"]— six entries.dormouse-libas a declared dependency: present instandalone/package.jsonandwebsite/package.json, absent fromvscode-ext/package.json(whosedependenciesarenode-ptyandwsonly).grep -rl '\.\./lib/src' vscode-ext/src/→ 15 files.git ls-tree --name-only HEADcross-checked against the three scope lists;.vscode/and.impeccable/were the only two absent, matching the audit'sWARNING.website/'s tracked top-level entries arepublic,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 runspnpm build:vscodevia"type": "shell")..impeccable/contents:design.json(aschemaVersion: 2design-token document — color roles, tonal ramps).node scripts/spec-lint.mjs→OK (23 specs, 24 files checked). Docs-only change; no spec underdocs/specs/is touched..github/audit/puts this commit inworkflow-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 #447FAILaredefault_workflow_permissions: write/can_approve_pull_request_reviews: true(an admin toggle) andtend-mention.yaml'sverifyjob having nopermissions:block (an upstream generator gap, regenerated on every nightly). Both are analyzed in the nightly comment on #447.