@oomkapwn/enquire-mcp 3.7.12 → 3.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +5 -5
  3. package/dist/http-transport.d.ts +11 -0
  4. package/dist/http-transport.d.ts.map +1 -1
  5. package/dist/http-transport.js +34 -0
  6. package/dist/http-transport.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/pdf.d.ts +22 -1
  10. package/dist/pdf.d.ts.map +1 -1
  11. package/dist/pdf.js +9 -2
  12. package/dist/pdf.js.map +1 -1
  13. package/dist/tool-registry.d.ts.map +1 -1
  14. package/dist/tool-registry.js +26 -1
  15. package/dist/tool-registry.js.map +1 -1
  16. package/dist/tools/media.d.ts.map +1 -1
  17. package/dist/tools/media.js +15 -6
  18. package/dist/tools/media.js.map +1 -1
  19. package/dist/tools/write.d.ts.map +1 -1
  20. package/dist/tools/write.js +46 -10
  21. package/dist/tools/write.js.map +1 -1
  22. package/dist/vault.d.ts.map +1 -1
  23. package/dist/vault.js +38 -14
  24. package/dist/vault.js.map +1 -1
  25. package/docs/COMPARISON.md +1 -1
  26. package/docs/QUICKSTART.md +2 -2
  27. package/docs/api-reference/functions/index.buildEmbedText.html +1 -1
  28. package/docs/api-reference/functions/index.buildMcpServer.html +1 -1
  29. package/docs/api-reference/functions/index.formatReadyBanner.html +1 -1
  30. package/docs/api-reference/functions/index.main.html +1 -1
  31. package/docs/api-reference/functions/index.parsePositiveInt.html +1 -1
  32. package/docs/api-reference/functions/index.parseQuantizationMode.html +1 -1
  33. package/docs/api-reference/functions/index.prepareServerDeps.html +1 -1
  34. package/docs/api-reference/functions/index.startServer.html +1 -1
  35. package/docs/api-reference/functions/tools.appendToNote.html +1 -1
  36. package/docs/api-reference/functions/tools.archiveNote.html +1 -1
  37. package/docs/api-reference/functions/tools.assertHnswModelMatchesEmbedder.html +1 -1
  38. package/docs/api-reference/functions/tools.chatThreadAppend.html +1 -1
  39. package/docs/api-reference/functions/tools.chatThreadRead.html +1 -1
  40. package/docs/api-reference/functions/tools.contextPack.html +1 -1
  41. package/docs/api-reference/functions/tools.createNote.html +1 -1
  42. package/docs/api-reference/functions/tools.dataviewQuery.html +1 -1
  43. package/docs/api-reference/functions/tools.embeddingsSearch.html +1 -1
  44. package/docs/api-reference/functions/tools.findPath.html +1 -1
  45. package/docs/api-reference/functions/tools.findSimilar.html +1 -1
  46. package/docs/api-reference/functions/tools.frontmatterGet.html +1 -1
  47. package/docs/api-reference/functions/tools.frontmatterSearch.html +1 -1
  48. package/docs/api-reference/functions/tools.frontmatterSet.html +1 -1
  49. package/docs/api-reference/functions/tools.getBacklinks.html +1 -1
  50. package/docs/api-reference/functions/tools.getNoteNeighbors.html +1 -1
  51. package/docs/api-reference/functions/tools.getOpenQuestions.html +1 -1
  52. package/docs/api-reference/functions/tools.getOutboundLinks.html +1 -1
  53. package/docs/api-reference/functions/tools.getRecentEdits.html +1 -1
  54. package/docs/api-reference/functions/tools.getUnresolvedWikilinks.html +1 -1
  55. package/docs/api-reference/functions/tools.getVaultStats.html +1 -1
  56. package/docs/api-reference/functions/tools.lintWiki.html +1 -1
  57. package/docs/api-reference/functions/tools.listCanvases.html +1 -1
  58. package/docs/api-reference/functions/tools.listNotes.html +1 -1
  59. package/docs/api-reference/functions/tools.listPdfs.html +1 -1
  60. package/docs/api-reference/functions/tools.listTags.html +1 -1
  61. package/docs/api-reference/functions/tools.ocrPdf.html +1 -1
  62. package/docs/api-reference/functions/tools.openInUi.html +1 -1
  63. package/docs/api-reference/functions/tools.paperAudit.html +1 -1
  64. package/docs/api-reference/functions/tools.pickEmbedTextForHyde.html +1 -1
  65. package/docs/api-reference/functions/tools.readCanvas.html +1 -1
  66. package/docs/api-reference/functions/tools.readNote.html +1 -1
  67. package/docs/api-reference/functions/tools.readPdf.html +1 -1
  68. package/docs/api-reference/functions/tools.renameNote.html +1 -1
  69. package/docs/api-reference/functions/tools.replaceInNotes.html +1 -1
  70. package/docs/api-reference/functions/tools.resolveTarget.html +1 -1
  71. package/docs/api-reference/functions/tools.resolveWikilink.html +1 -1
  72. package/docs/api-reference/functions/tools.searchHybrid.html +1 -1
  73. package/docs/api-reference/functions/tools.searchText.html +1 -1
  74. package/docs/api-reference/functions/tools.semanticSearch.html +1 -1
  75. package/docs/api-reference/functions/tools.validateNoteProposal.html +1 -1
  76. package/docs/api-reference/interfaces/index.ServeOptions.html +25 -25
  77. package/docs/api-reference/interfaces/index.ServerDeps.html +3 -3
  78. package/docs/api-reference/interfaces/tool-manifest.ToolManifestEntry.html +5 -5
  79. package/docs/api-reference/interfaces/tools.ArchiveNoteArgs.html +5 -5
  80. package/docs/api-reference/interfaces/tools.BacklinkHit.html +6 -6
  81. package/docs/api-reference/interfaces/tools.CanvasEdge.html +8 -8
  82. package/docs/api-reference/interfaces/tools.CanvasSummary.html +7 -7
  83. package/docs/api-reference/interfaces/tools.ChatThreadAppendArgs.html +5 -5
  84. package/docs/api-reference/interfaces/tools.ChatThreadMessage.html +6 -6
  85. package/docs/api-reference/interfaces/tools.ChatThreadReadResult.html +5 -5
  86. package/docs/api-reference/interfaces/tools.ContextPackArgs.html +6 -6
  87. package/docs/api-reference/interfaces/tools.ContextPackResult.html +7 -7
  88. package/docs/api-reference/interfaces/tools.EmbedHit.html +9 -9
  89. package/docs/api-reference/interfaces/tools.EmbedSearchResponse.html +2 -2
  90. package/docs/api-reference/interfaces/tools.FindPathResult.html +7 -7
  91. package/docs/api-reference/interfaces/tools.FrontmatterSearchArgs.html +7 -7
  92. package/docs/api-reference/interfaces/tools.FrontmatterSetArgs.html +5 -5
  93. package/docs/api-reference/interfaces/tools.HnswSearchContext.html +3 -3
  94. package/docs/api-reference/interfaces/tools.LintWikiArgs.html +6 -6
  95. package/docs/api-reference/interfaces/tools.LintWikiFinding.html +6 -6
  96. package/docs/api-reference/interfaces/tools.LintWikiResult.html +2 -2
  97. package/docs/api-reference/interfaces/tools.NoteNeighbors.html +5 -5
  98. package/docs/api-reference/interfaces/tools.NoteReadFull.html +9 -9
  99. package/docs/api-reference/interfaces/tools.NoteReadMap.html +11 -11
  100. package/docs/api-reference/interfaces/tools.NoteSummary.html +6 -6
  101. package/docs/api-reference/interfaces/tools.OcrPdfArgs.html +5 -5
  102. package/docs/api-reference/interfaces/tools.OcrPdfPage.html +6 -6
  103. package/docs/api-reference/interfaces/tools.OcrPdfResult.html +4 -4
  104. package/docs/api-reference/interfaces/tools.OpenInUiResult.html +5 -5
  105. package/docs/api-reference/interfaces/tools.OpenQuestion.html +8 -8
  106. package/docs/api-reference/interfaces/tools.OutboundLink.html +9 -9
  107. package/docs/api-reference/interfaces/tools.PaperAuditFinding.html +7 -7
  108. package/docs/api-reference/interfaces/tools.PathStep.html +4 -4
  109. package/docs/api-reference/interfaces/tools.PdfSummary.html +5 -5
  110. package/docs/api-reference/interfaces/tools.ReadCanvasResult.html +3 -3
  111. package/docs/api-reference/interfaces/tools.ReadPdfArgs.html +4 -4
  112. package/docs/api-reference/interfaces/tools.ReadPdfPage.html +5 -5
  113. package/docs/api-reference/interfaces/tools.ReadPdfResult.html +3 -3
  114. package/docs/api-reference/interfaces/tools.RenameNoteResult.html +6 -6
  115. package/docs/api-reference/interfaces/tools.RenameProposal.html +5 -5
  116. package/docs/api-reference/interfaces/tools.ReplaceInNotesArgs.html +6 -6
  117. package/docs/api-reference/interfaces/tools.ReplaceInNotesFileResult.html +3 -3
  118. package/docs/api-reference/interfaces/tools.ReplaceInNotesResult.html +4 -4
  119. package/docs/api-reference/interfaces/tools.SearchHit.html +6 -6
  120. package/docs/api-reference/interfaces/tools.SearchHybridHit.html +9 -9
  121. package/docs/api-reference/interfaces/tools.SearchHybridResponse.html +6 -6
  122. package/docs/api-reference/interfaces/tools.SearchResponse.html +5 -5
  123. package/docs/api-reference/interfaces/tools.SemanticHit.html +7 -7
  124. package/docs/api-reference/interfaces/tools.SimilarNote.html +7 -7
  125. package/docs/api-reference/interfaces/tools.TagSummary.html +5 -5
  126. package/docs/api-reference/interfaces/tools.UnresolvedWikilink.html +10 -10
  127. package/docs/api-reference/interfaces/tools.ValidateProposalArgs.html +4 -4
  128. package/docs/api-reference/interfaces/tools.ValidateProposalResult.html +2 -2
  129. package/docs/api-reference/interfaces/tools.VaultStats.html +11 -11
  130. package/docs/api-reference/types/tools.CanvasNode.html +1 -1
  131. package/docs/api-reference/types/tools.SearchMode.html +1 -1
  132. package/docs/api-reference/variables/index.VERSION.html +1 -1
  133. package/docs/api-reference/variables/tool-manifest.TOOL_MANIFEST.html +1 -1
  134. package/docs/api.md +1 -1
  135. package/docs/benchmarks.md +16 -9
  136. package/package.json +9 -4
  137. package/docs/audits/findings/L1-code-quality.md +0 -213
  138. package/docs/audits/findings/L2-architecture.md +0 -245
  139. package/docs/audits/findings/L3-tests.md +0 -339
  140. package/docs/audits/findings/L4-cicd.md +0 -290
  141. package/docs/audits/findings/L5-security.md +0 -350
  142. package/docs/audits/findings/L6-documentation.md +0 -347
  143. package/docs/audits/findings/L7-operational.md +0 -50
  144. package/docs/audits/findings/L8-reproducibility.md +0 -64
  145. package/docs/audits/findings/L9-process.md +0 -84
  146. package/docs/audits/findings/baseline.json +0 -19
  147. package/docs/audits/v3.6.0-external-anonymous-audit.md +0 -163
  148. package/docs/audits/v3.6.0-final-audit.md +0 -171
  149. package/docs/audits/v3.6.0-rc.4-rootcause.md +0 -134
  150. package/docs/audits/v3.6.0-system-audit-plan.md +0 -199
@@ -1,350 +0,0 @@
1
- # L5 — Security (v3.6.0 audit)
2
-
3
- **Scope**: CodeQL, Dependabot, npm audit, SLSA-3 provenance, bearer auth, path traversal, privacy filters, cache permissions.
4
- **Auditor**: sub-agent C5.
5
- **Date**: 2026-05-15.
6
- **Repo**: `oomkapwn/enquire-mcp` (local folder name is `obsidian-mcp`, project name is `enquire-mcp`).
7
- **Branch**: `v3.6.0/post-stable-audit`.
8
- **Baseline package**: `@oomkapwn/enquire-mcp@3.6.0` (published 2026-05-15).
9
-
10
- ## Summary
11
-
12
- The security posture is strong. Every external check is clean: 0 open CodeQL alerts, 0 open Dependabot alerts, 0 npm-audit findings at every audit level (`--audit-level=low/moderate/high` × dev/prod). SLSA-3 provenance attestation IS emitted for v3.6.0. Bearer auth uses `crypto.timingSafeEqual` after hashing both sides, no naive `===` comparison. CORS implementation explicitly defends against the `js/cors-misconfiguration-for-credentials` class. Path traversal goes through `vault.resolveSafePath()` with `fs.realpath` checks on every read/write; symlink-escape rejected at both parent-dir and leaf-target. Privacy filters (`--exclude-glob` / `--read-paths`) applied at 11+ surfaces: listMarkdown, listFilesByExtension, resolveSafePath, writeNote, renameFile, watcher (chokidar `ignored` predicate), text search post-filter, chunk resource gate, FTS5 hybrid post-filter, embeddings post-filter, replace_in_notes folder check. Cache files (.embed.db, .fts5.db, persistent-note-cache) consistently chmod 0600 with 0700 parent dirs.
13
-
14
- The findings below cluster into 3 classes, all defense-in-depth (no exploitable issues):
15
-
16
- 1. **L5-01 (Medium)** — HNSW persistence files (`.hnsw.bin` + `.hnsw.meta.json`) are written without explicit 0600 chmod, defaulting to 0644 (umask-modified). The `.meta.json` contains note path + text-preview snippets. Parent dir is implicitly 0700 (shared with `.embed.db`'s open path), but the files themselves break the pattern set by `embed-db.ts`, `fts5.ts`, and `vault.ts`.
17
- 2. **L5-02 (Medium)** — `enquire-mcp setup` and `enquire-mcp index` CLI commands instantiate `Vault` WITHOUT `excludeGlobs` / `readPaths`, while `serve` and `build-embeddings` accept those flags. A user who runs `setup --vault foo` then later runs `serve --exclude-glob` ends up with FTS5 chunks for excluded paths persisted on disk in the `.fts5.db`. Runtime search filters those out via `vault.isExcluded()`, so an LLM never receives them — but at-rest content of excluded paths lives in the index file, contrary to the SECURITY.md "denylist" expectation.
18
- 3. **L5-03 (Info)** — 5 dismissed CodeQL alerts (`js/polynomial-redos` #5, #6, #8, #9, #10) all share the same dismissed_comment template. Code at the cited lines is unchanged since dismissal (2026-05-13); inline reasoning still holds (anchored `$` regex on single char class, strictly linear). No action needed; called out only for traceability.
19
-
20
- No Critical / High findings. No exploitable issues. Both Mediums are defense-in-depth — they don't expose excluded content over the wire, they just leave artifacts on disk with weaker permissions or in caches the user expected to be empty.
21
-
22
- ---
23
-
24
- ## Scope-item-by-scope-item verification
25
-
26
- ### 1. CodeQL alerts
27
-
28
- `gh api repos/oomkapwn/enquire-mcp/code-scanning/alerts --jq '[.[] | select(.state == "open")] | length'` → **0**.
29
-
30
- 5 dismissed alerts, all `js/polynomial-redos`, all with the same template comment:
31
- > Anchored-$ regex on single char class — strictly linear (O(n) worst case, no backtracking branch). Same class as v3.5.8 chunker.ts/bases.ts dismissals. See inline comments in src for per-site reasoning.
32
-
33
- | # | Path | Line | Pattern | Dismissed at | Inline comment still present? |
34
- |---|------|------|---------|--------------|-------------------------------|
35
- | 5 | `src/embed-db.ts` | 407 | `/\/+$/` (folder-prefix trim) | 2026-05-13 | Yes — `src/embed-db.ts:403-406` |
36
- | 6 | `src/fts5.ts` | 377 | `/\/+$/` (folder-prefix trim) | 2026-05-13 | Yes — `src/fts5.ts:373-376` |
37
- | 8 | `src/fts5.ts` | 596 | `/^(#{1,6})\s+(.+)$/` (heading parse) | 2026-05-13 | Yes — `src/fts5.ts:591-595` |
38
- | 9 | `src/fts5.ts` | 599 | `/\s+$/` (heading trim) | 2026-05-13 | Yes — `src/fts5.ts:591-595` |
39
- | 10 | `src/fts5.ts` | 599 | `/#+$/` (heading trim) | 2026-05-13 | Yes — `src/fts5.ts:591-595` |
40
-
41
- `git log --since="2026-05-13" -- src/embed-db.ts src/fts5.ts` returns no commits. Dismissed reasoning is still accurate.
42
-
43
- ### 2. Dependabot alerts
44
-
45
- `gh api repos/oomkapwn/enquire-mcp/dependabot/alerts --jq '[.[] | select(.state == "open")] | length'` → **0**.
46
-
47
- Upgrade policy in `.github/dependabot.yml`:
48
- - Weekly cadence, Monday 06:00 Moscow time.
49
- - Open-PR limit: 5 (npm) + 3 (gh-actions).
50
- - Groups dev-deps (minor/patch) and runtime-patches separately — no auto-merge configured (PRs require human review).
51
- - Production major bumps land as individual PRs (not grouped), so risky upgrades get individual scrutiny.
52
-
53
- Upgrade policy is reasonable; no auto-merge means the human-review gate is intact.
54
-
55
- ### 3. npm audit
56
-
57
- | Command | Result |
58
- |---------|--------|
59
- | `npm audit --omit=dev --audit-level=moderate` | `found 0 vulnerabilities` |
60
- | `npm audit --include=dev --audit-level=high` | `found 0 vulnerabilities` |
61
- | `npm audit --include=dev` (low) | `found 0 vulnerabilities` |
62
-
63
- Zero findings at every level — the dependency tree is clean.
64
-
65
- ### 4. SLSA-3 provenance
66
-
67
- `npm view @oomkapwn/enquire-mcp@latest --json | jq '.dist'` returns:
68
- ```json
69
- {
70
- "attestations": {
71
- "url": "https://registry.npmjs.org/-/npm/v1/attestations/@oomkapwn%2fenquire-mcp@3.6.0",
72
- "provenance": { "predicateType": "https://slsa.dev/provenance/v1" }
73
- },
74
- "signatures": [
75
- { "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", ... }
76
- ]
77
- }
78
- ```
79
-
80
- `.attestations.provenance.predicateType` is `slsa.dev/provenance/v1` — full SLSA-3 provenance attached. Same for `@oomkapwn/enquire-mcp@3.6.0` exact version.
81
-
82
- Release workflow (`.github/workflows/release.yml:14-16`) declares `id-token: write` permission; publish step (line 117) uses `npm publish --provenance --access public`. Confirmed.
83
-
84
- ### 5. Bearer auth — constant-time comparison
85
-
86
- `grep -n 'timingSafeEqual\|=== bearerToken\|===.*token' src/http-transport.ts`:
87
-
88
- - Line 31: `import { createHash, randomBytes, timingSafeEqual } from "node:crypto";`
89
- - Line 161: `const expectedHash = createHash("sha256").update(expectedToken).digest();`
90
- - Line 162: `const presentedHash = createHash("sha256").update(presented).digest();`
91
- - Line 163: `if (!timingSafeEqual(expectedHash, presentedHash)) return null;`
92
-
93
- `verifyBearer()` (`src/http-transport.ts:154-167`) hashes both sides to fixed-length SHA-256 buffers before `timingSafeEqual`, defeating length-leak side channels. Token validation is constant-time. No naive `===` comparison anywhere in the file (only `=== null` for the verifyBearer return value, line 358).
94
-
95
- Generation: `generateBearerToken()` (line 582) → `randomBytes(32).toString("base64url")` → ~43 char base64url, CSPRNG. Startup gate (line 591): rejects `bearerToken < 16 chars`.
96
-
97
- ### 6. Path traversal — realpath checks
98
-
99
- Every Vault file operation routes through one of two entry points:
100
-
101
- - **`Vault.resolveInside(p)`** (`src/vault.ts:292-299`) — pure lexical check (`path.relative` rejects `..` or absolute escapes). Used for non-existent-file paths (writes to new files).
102
- - **`Vault.resolveSafePath(relOrAbs)`** (`src/vault.ts:573-610`) — realpath-after-resolve, rejects if resolved path escapes vault root; also enforces `isExcluded`.
103
-
104
- Direct `fs.readFile` / `fs.writeFile` outside `vault.ts`:
105
-
106
- ```
107
- src/eval.ts:145 fs.readFile(file, "utf8") # user-supplied JSONL, CLI-only diagnostic tool
108
- src/hnsw.ts:309 fs.writeFile(metaFile, ...) # HNSW persistence (see L5-01)
109
- src/hnsw.ts:337 fs.readFile(metaFile, "utf8") # HNSW load (own cache file)
110
- src/periodic.ts:64 fs.readFile(dailyJsonPath) # .obsidian/daily-notes.json — gated by isExcluded
111
- src/periodic.ts:84 fs.readFile(periodicJsonPath) # .obsidian/plugins/periodic-notes/data.json — gated
112
- ```
113
-
114
- `eval.ts:145` — CLI-only `enquire eval` diagnostic; reads a user-supplied JSONL path. Not user-controllable via MCP. Not a concern.
115
- `hnsw.ts:309/337` — owns the HNSW sidecar files at known-good paths (`<embedDir>/<vaultname>.hnsw.bin` + `.meta.json`); not influenced by note content. See L5-01 for chmod.
116
- `periodic.ts:64/84` — both calls gated by an `isExcluded` predicate (`src/periodic.ts:62`, `src/periodic.ts:80`); both target fixed `.obsidian/...` paths that can't be redirected.
117
-
118
- Symlink-escape protection: `Vault.assertParentInsideVault` (`src/vault.ts:446-459`) walks parent chain with `fs.realpath`, refuses writes if any parent resolves outside. Leaf-target symlinks rejected explicitly (`src/vault.ts:431-434`). Walker `followSymlinks: false`.
119
-
120
- Path traversal class fully defended.
121
-
122
- ### 7. Privacy filters
123
-
124
- `--exclude-glob` and `--read-paths` are applied at the following surfaces (file:line citations):
125
-
126
- | Surface | Function | File:line | Notes |
127
- |---|---|---|---|
128
- | 1 | Vault `listMarkdown` | `src/vault.ts:321-323` | Post-walk filter. Also gates folder-arg via `isExcluded(rel)` on line 313 |
129
- | 2 | Vault `listFilesByExtension` | `src/vault.ts:344-346` | Same pattern as listMarkdown, also gates folder-arg (line 340) |
130
- | 3 | Vault `resolveSafePath` (read path) | `src/vault.ts:598-604` | Refuses with allowlist-vs-denylist distinction in error message |
131
- | 4 | Vault `writeNote` | `src/vault.ts:412-418` | Pre-write enforcement (P0 fix from v2.0.0-beta.1); both allowlist + denylist |
132
- | 5 | Vault `renameFile` (target) | `src/vault.ts:481-485` | Same predicate as writeNote |
133
- | 6 | `VaultWatcher.start` (chokidar predicate) | `src/watcher.ts:54-58` | Watcher never sees writes to excluded files (no FTS5 reindex trigger, no cache invalidation reveal) |
134
- | 7 | `tool-registry.ts` text-search results | `src/tool-registry.ts:104` | Post-filter on `searchText`-style hits |
135
- | 8 | `tool-registry.ts` chunk resource | `src/tool-registry.ts:1188` | "Chunk not found" framing matches the not-found branch — attacker can't distinguish |
136
- | 9 | `tools/search.ts` `embeddingsSearch` | `src/tools/search.ts:908` | Post-filter on embed-cosine hits + HNSW results |
137
- | 10 | `tools/search.ts` `searchHybrid` FTS5 leg | `src/tools/search.ts:1151` | Filters BM25 hits before RRF fusion — stale entries from pre-flag indexes blocked |
138
- | 11 | `tools/write.ts` `replace_in_notes` folder | `src/tools/write.ts:577-582` | Tests both `<folder>` and `<folder>/_probe.md` to handle `**`-glob semantics |
139
- | 12 | `periodic.ts` config loader | `src/periodic.ts:62, 80` | `.obsidian/daily-notes.json` and `.obsidian/plugins/periodic-notes/data.json` both gated |
140
-
141
- **Trace 1 — FTS5 indexing**: `syncFtsIndex` (`src/server.ts:678`) calls `vault.listMarkdown()` which filters via `isExcluded` at `src/vault.ts:322`. Indexed entries never include excluded paths (at build time, with privacy flags wired through the relevant Vault constructor).
142
-
143
- **Trace 2 — Embeddings build**: `syncEmbedDb` (`src/server.ts:567`) calls `vault.listMarkdown()` — same filter point. Chunker (`chunkContent` in `src/fts5.ts:502`) receives only already-vetted content; no separate filter needed.
144
-
145
- **Trace 3 — Hybrid search at query time**: `searchHybrid` (`src/tools/search.ts`) calls FTS5 + TF-IDF + embeddings; each leg filters via `vault.isExcluded()` (line 1151 + line 908 + via TF-IDF's `buildTfidfIndex` which uses `vault.listMarkdown`). Plus, even if `.fts5.db` contained stale excluded entries from a pre-flag setup, the runtime filter strips them before RRF fusion. Defense-in-depth holds.
146
-
147
- **Trace 4 — TF-IDF**: `buildTfidfIndex` (`src/tools/search.ts:484-487`) uses `vault.listMarkdown()` directly; index built only from non-excluded files. Per-query post-filter not needed because the index never contained them.
148
-
149
- **Trace 5 — Tool resources (chunk URI)**: `tool-registry.ts:1188` blocks `enquire://chunk/...` URIs for excluded paths, even if those URIs were issued earlier in the session when no exclude flag was active.
150
-
151
- Privacy filter coverage is complete at every code path I traced. See L5-02 for an at-rest-only concern around `setup` / `index` CLI commands.
152
-
153
- ### 8. Cache permissions
154
-
155
- Cache files (verified):
156
-
157
- | File | chmod 0600 | Parent dir chmod 0700 | Source |
158
- |---|---|---|---|
159
- | `.embed.db` + `-wal` + `-shm` | Yes | Yes | `src/embed-db.ts:210, 211, 217` |
160
- | `.fts5.db` + `-wal` + `-shm` | Yes | Yes | `src/fts5.ts:125, 126, 135` |
161
- | Persistent note cache (JSON) | Yes (0600 on write + chmod) | Yes (0700 on mkdir + chmod) | `src/vault.ts:277, 282, 285, 288` |
162
- | `.hnsw.bin` (binary index) | **No (defaults to 0644 via umask)** | Implicit 0700 (shares dir with `.embed.db` if EmbedDb opened first) | `src/hnsw.ts:300` (no chmod) |
163
- | `.hnsw.meta.json` (path + text-preview rows) | **No (defaults to 0644 via umask)** | Same as above | `src/hnsw.ts:309` (no chmod) |
164
-
165
- See L5-01 for the HNSW chmod gap.
166
-
167
- ---
168
-
169
- ## Findings detail
170
-
171
- ### Finding L5-01 (Medium)
172
-
173
- **File**: `src/hnsw.ts:289-312` (`saveTo` method, the inner of `wrapNativeIndex`).
174
- **Class**: Cache-file permission drift — sidecar files written via `fs.writeFile` or native libs without an explicit `mode` argument or post-write `chmod`. The strict 0600/0700 invariant enforced in `embed-db.ts`, `fts5.ts`, and `vault.ts` is broken at the HNSW persistence path.
175
- **Severity**: Medium (defense-in-depth — files live in a 0700 parent dir created by `EmbedDb.open()`, so a sibling user can't traverse in unless they were granted access at the parent level. But the SECURITY.md "0600 cache" guarantee doesn't apply to HNSW sidecar files, and a fresh HNSW save creates files under the user's umask, which on shared / NFS / corporate-image systems can be 0664 or 0644).
176
-
177
- **Description**: When the HNSW index is persisted, the workflow is:
178
-
179
- ```ts
180
- // src/hnsw.ts:289-310
181
- async saveTo(file, rowsByLabel, signature): Promise<boolean> {
182
- const fs = await import("node:fs/promises");
183
- const path = await import("node:path");
184
- await fs.mkdir(path.dirname(file), { recursive: true }); // ← no mode: 0o700
185
- const binFile = `${file}.bin`;
186
- const metaFile = `${file}.meta.json`;
187
- await ctor.writeIndex(binFile); // ← native lib write, no chmod after
188
- const meta: HnswPersistedMeta = { ... };
189
- await fs.writeFile(metaFile, JSON.stringify(meta, null, 2), "utf8"); // ← no mode option, no chmod after
190
- return true;
191
- }
192
- ```
193
-
194
- The `.hnsw.meta.json` payload includes (`src/hnsw.ts:82-92`):
195
- - `rel_path` (vault-relative path to every chunk)
196
- - `text_preview` (note-content snippet, up to 480 chars per chunk)
197
- - `chunk_index`, `line_start`, `line_end`, `kind`
198
-
199
- This is the same class of sensitive metadata that `.embed.db` and `.fts5.db` already protect with 0600. The HNSW sidecar leaks it under a more permissive default mode.
200
-
201
- Mitigating factor: HNSW files are written to `<embedDir>/<vaultname>.hnsw.{bin,meta.json}`, and `embedDir` is set to 0700 by `EmbedDb.connect()` at `src/embed-db.ts:210-211`. So in practice, file mode 0644 is overridden by the parent dir's 0700 — a sibling user can't `cd` in to read them. BUT: (a) defense-in-depth wants both layers, (b) `saveTo` is also called when the user has not run `EmbedDb.open()` for this exact dir before (parent might not exist), (c) some filesystems (NFS, FAT-on-USB) ignore Unix mode bits — the parent-dir guarantee evaporates.
202
-
203
- **Class**: Same-class instances of "writes cache file without explicit chmod":
204
- - `src/hnsw.ts:295` `fs.mkdir` — missing `mode: 0o700` (other call-sites set it: `src/embed-db.ts:210`, `src/fts5.ts:125`, `src/vault.ts:277`).
205
- - `src/hnsw.ts:300` `ctor.writeIndex(binFile)` — native lib write, no post-write `chmod`.
206
- - `src/hnsw.ts:309` `fs.writeFile(metaFile, ...)` — no `mode` option in the third arg, no post-write `chmod`.
207
-
208
- **Class fix**:
209
- 1. In `src/hnsw.ts:saveTo`, mirror the pattern from `src/embed-db.ts:207-219`:
210
- ```ts
211
- await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
212
- await fs.chmod(path.dirname(file), 0o700).catch(() => {});
213
- await ctor.writeIndex(binFile);
214
- await fs.writeFile(metaFile, JSON.stringify(meta, null, 2), { encoding: "utf8", mode: 0o600 });
215
- await Promise.all([binFile, metaFile].map((p) => fs.chmod(p, 0o600).catch(() => {})));
216
- ```
217
- 2. Add a test in `tests/hnsw.test.ts` (if it exists) or a new test that asserts mode bits after `saveTo`. See `tests/embed-db.test.ts` for the pattern (assertions on `stat.mode & 0o777`).
218
- 3. Document the chmod-on-cache invariant in `CLAUDE.md` or a new `docs/internals/cache-permissions.md` so future cache-file additions get audited.
219
-
220
- **Backfill**: Single instance; the fix above resolves L5-01.
221
-
222
- **Recommendation**: Ship in v3.6.1. Low complexity, no behavior change for the common case.
223
-
224
- ---
225
-
226
- ### Finding L5-02 (Medium)
227
-
228
- **File**: `src/cli.ts:298` (`enquire-mcp index` command), `src/cli.ts:487` (`enquire-mcp setup` command).
229
- **Class**: CLI-flag drift — privacy flags (`--exclude-glob` / `--read-paths`) accepted by `serve`, `serve-http`, and `build-embeddings`, but NOT by `setup` and `index`. A user who runs `setup --vault foo` (the documented "zero-touch onboarding" path) gets a `.fts5.db` containing chunks of every file in their vault, including any path they later want to mark private.
230
-
231
- **Severity**: Medium (the runtime filter at `tools/search.ts:1151` strips excluded paths from search results, so an LLM never receives them — but at-rest content of supposedly-private notes lives in `.fts5.db` and `.embed.db` with 0600 perms, contrary to the SECURITY.md "privacy filter at indexing time" guarantee).
232
-
233
- **Description**:
234
-
235
- ```ts
236
- // src/cli.ts:298 — `index` subcommand
237
- const vault = new Vault(opts.vault); // no excludeGlobs, no readPaths
238
- ```
239
-
240
- ```ts
241
- // src/cli.ts:487 — `setup` subcommand
242
- const v = new Vault(opts.vault); // no excludeGlobs, no readPaths
243
- ```
244
-
245
- Compare to:
246
-
247
- ```ts
248
- // src/cli.ts:384 — `build-embeddings` (has flags)
249
- const vault = new Vault(opts.vault, { excludeGlobs: opts.excludeGlob, readPaths: opts.readPaths });
250
- ```
251
-
252
- ```ts
253
- // src/cli.ts:57-62 — `serve` accepts these flags
254
- program.option("--exclude-glob <pattern...>", "...");
255
- program.option("--read-paths <pattern...>", "...");
256
- ```
257
-
258
- So the CLI surface is inconsistent: `serve` + `build-embeddings` honor privacy, `setup` + `index` don't.
259
-
260
- Practical attack scenario: User runs `enquire-mcp setup --vault ~/Notes` (recommended in README QUICKSTART). Cold-built `.fts5.db` and `.embed.db` now contain every file. User then writes a script that runs `enquire-mcp serve --vault ~/Notes --exclude-glob '02_Private/**'`. At runtime, search results are filtered. But:
261
- 1. Any other process that opens the `.fts5.db` directly (a SQLite client, a `sqlite3` shell, the `enquire-mcp dump-index` command if one exists, leaked backup) sees all the private chunks.
262
- 2. If the user later removes the `--exclude-glob` flag, the index already has the private chunks — no rebuild needed for them to surface.
263
- 3. SECURITY.md section `--read-paths: strict-allowlist posture` (line 50-59) implies the filter is enforced at every layer, not just runtime.
264
-
265
- **Class**: Same-class instances of "Vault constructor not threading user privacy flags":
266
- - `src/cli.ts:266` (`clear-index` — Vault used only for `defaultIndexFile()` path derivation; no file content access). **Not a finding** — just path resolution.
267
- - `src/cli.ts:298` (`index`). **Finding**.
268
- - `src/cli.ts:425` (`clear-embeddings` — same, path-only). **Not a finding**.
269
- - `src/cli.ts:487` (`setup`). **Finding**.
270
- - `src/cli.ts:607` (`eval` — diagnostic / benchmark; query set is explicit). **Not a finding** — eval is intended to exercise the full corpus for retrieval quality measurement.
271
-
272
- **Class fix**:
273
- 1. Add `--exclude-glob` and `--read-paths` options to both `index` (`src/cli.ts:283-295`) and `setup` (`src/cli.ts:468-485`) commands.
274
- 2. Thread them through the `new Vault(...)` constructor at lines 298 and 487 (matching the pattern at `src/cli.ts:384`).
275
- 3. Add a guard in `setup` that warns when a user runs `setup` without privacy flags but their `serve` invocations elsewhere DO use them. Alternative: add a `--re-setup-needed` notice on `serve` start when the index mtime predates the privacy flags being introduced. (Low priority — main fix is just threading the flags.)
276
- 4. Add a CHANGELOG-tracked invariant: "every CLI command that opens a Vault for indexing must accept `--exclude-glob` and `--read-paths`."
277
- 5. Update `docs/QUICKSTART.md` to show `enquire-mcp setup --vault ~/Notes --exclude-glob '02_Private/**'` as the privacy-aware default.
278
-
279
- **Backfill**: Two instances (`src/cli.ts:298`, `src/cli.ts:487`). Plus an integration test that runs `enquire-mcp setup --vault tmp --exclude-glob 'Secret/**'` and asserts the `.fts5.db` doesn't contain any rows where `rel_path` matches the exclude glob.
280
-
281
- **Recommendation**: Ship in v3.6.1 alongside L5-01. Could be batched as a single "v3.6.1: hardening" release.
282
-
283
- ---
284
-
285
- ### Finding L5-03 (Info)
286
-
287
- **File**: 5 dismissed CodeQL alerts on `oomkapwn/enquire-mcp/security/code-scanning`.
288
- **Class**: CodeQL `js/polynomial-redos` false positives on anchored `$` regexes over single character classes. Same class as the v3.5.8 `chunker.ts` / `bases.ts` dismissals (referenced in the dismissed_comment template).
289
- **Severity**: Info — no action needed. Captured here so the v3.7+ auditor can confirm at re-audit time that the dismissed_comment template still applies.
290
-
291
- **Description**: All 5 alerts dismissed 2026-05-13 by Alex with the same inline-reasoning comment template ("Anchored-$ regex on single char class — strictly linear..."). I verified each alert's cited source line and confirmed:
292
-
293
- 1. Code at the cited lines is **unchanged** since dismissal — `git log --since="2026-05-13" -- src/embed-db.ts src/fts5.ts` is empty.
294
- 2. Each regex has an inline `// CodeQL js/polynomial-redos flags ... false positive...` comment in source pointing readers at the reasoning. Examples: `src/embed-db.ts:403-406`, `src/fts5.ts:373-376`, `src/fts5.ts:591-595`.
295
- 3. The regex patterns are all `/\/+$/`, `/\s+$/`, `/#+$/`, `/^(#{1,6})\s+(.+)$/` — all anchored, all character-class greedy with no nested quantifier. Linear-time by construction.
296
-
297
- **Class fix**: None — these are working as intended. The class invariant ("any new `$`-anchored regex on a single char class with a `// CodeQL js/polynomial-redos: anchored-$ ...` comment is acceptable; otherwise grep for backtrack-able combinators") should be added to CLAUDE.md for future auditors.
298
-
299
- **Backfill**: None.
300
-
301
- **Recommendation**: No action. Re-audit at v3.7+ to confirm code lines haven't shifted under the dismissed alerts.
302
-
303
- ---
304
-
305
- ## Verification commands (rerunnable)
306
-
307
- ```bash
308
- # CodeQL state
309
- gh api repos/oomkapwn/enquire-mcp/code-scanning/alerts --jq '[.[] | select(.state == "open")] | length'
310
- gh api repos/oomkapwn/enquire-mcp/code-scanning/alerts --jq '[.[] | select(.state == "dismissed")] | .[] | {number, rule: .rule.id, dismissed_comment, most_recent_instance: .most_recent_instance.location}'
311
-
312
- # Dependabot
313
- gh api repos/oomkapwn/enquire-mcp/dependabot/alerts --jq '[.[] | select(.state == "open")] | length'
314
-
315
- # npm audit (run from project root)
316
- npm audit --omit=dev --audit-level=moderate
317
- npm audit --include=dev --audit-level=high
318
- npm audit --include=dev # full
319
-
320
- # SLSA provenance
321
- npm view @oomkapwn/enquire-mcp@latest --json | jq '.dist.attestations'
322
-
323
- # Bearer auth
324
- grep -n 'timingSafeEqual\|=== bearerToken\|===.*token' src/http-transport.ts
325
-
326
- # Path traversal — direct fs calls outside vault.ts
327
- grep -rn 'fs\.readFile\|fs\.writeFile\|fsp\.readFile\|fsp\.writeFile' src/ | grep -v "vault.ts"
328
-
329
- # Privacy filter sites
330
- grep -rn 'isExcluded' src/
331
-
332
- # Cache file modes
333
- grep -n '0o600\|0o700\|chmod\|mode: 0' src/embed-db.ts src/fts5.ts src/vault.ts src/hnsw.ts
334
- ```
335
-
336
- ---
337
-
338
- ## Sign-off
339
-
340
- - CodeQL: 0 open, 5 dismissed with current reasoning.
341
- - Dependabot: 0 open.
342
- - npm audit: 0 findings at every level.
343
- - SLSA-3: v3.6.0 attestation present, `slsa.dev/provenance/v1` predicate confirmed.
344
- - Bearer auth: constant-time via SHA-256 + `timingSafeEqual`.
345
- - Path traversal: every read/write through `vault.resolveSafePath()` or `vault.resolveInside()`; 5 direct `fs.*` calls outside `vault.ts` reviewed, all benign.
346
- - Privacy filters: 11+ enforcement points traced; 4 distinct code paths cited (FTS5 build, embeddings build, hybrid search, TF-IDF build).
347
- - Cache permissions: 0600 / 0700 enforced for `.embed.db`, `.fts5.db`, persistent-cache; **gap at HNSW sidecar files** (L5-01).
348
- - Privacy flag CLI surface: **inconsistent** — `setup` and `index` don't accept them (L5-02).
349
-
350
- No Critical / High. Two Mediums (L5-01, L5-02) shipable in a single v3.6.1 hardening release.