kastell 2.2.6 → 2.3.0

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 (272) hide show
  1. package/.claude-plugin/plugin.json +5 -3
  2. package/CHANGELOG.md +112 -0
  3. package/README.md +34 -6
  4. package/README.tr.md +6 -6
  5. package/dist/commands/audit.d.ts +3 -2
  6. package/dist/commands/audit.d.ts.map +1 -1
  7. package/dist/commands/audit.js +88 -47
  8. package/dist/commands/audit.js.map +1 -1
  9. package/dist/commands/auth.d.ts.map +1 -1
  10. package/dist/commands/auth.js +14 -1
  11. package/dist/commands/auth.js.map +1 -1
  12. package/dist/commands/domain.d.ts.map +1 -1
  13. package/dist/commands/domain.js +18 -1
  14. package/dist/commands/domain.js.map +1 -1
  15. package/dist/commands/evidence.d.ts.map +1 -1
  16. package/dist/commands/evidence.js +2 -1
  17. package/dist/commands/evidence.js.map +1 -1
  18. package/dist/commands/fix.d.ts.map +1 -1
  19. package/dist/commands/fix.js +12 -4
  20. package/dist/commands/fix.js.map +1 -1
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +2 -1
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/lock.d.ts.map +1 -1
  25. package/dist/commands/lock.js +7 -2
  26. package/dist/commands/lock.js.map +1 -1
  27. package/dist/commands/maintain.d.ts.map +1 -1
  28. package/dist/commands/maintain.js +13 -0
  29. package/dist/commands/maintain.js.map +1 -1
  30. package/dist/commands/plugin.d.ts.map +1 -1
  31. package/dist/commands/plugin.js +5 -4
  32. package/dist/commands/plugin.js.map +1 -1
  33. package/dist/commands/snapshot.d.ts.map +1 -1
  34. package/dist/commands/snapshot.js +16 -0
  35. package/dist/commands/snapshot.js.map +1 -1
  36. package/dist/commands/status.d.ts.map +1 -1
  37. package/dist/commands/status.js +2 -0
  38. package/dist/commands/status.js.map +1 -1
  39. package/dist/commands/update.d.ts.map +1 -1
  40. package/dist/commands/update.js +30 -10
  41. package/dist/commands/update.js.map +1 -1
  42. package/dist/core/audit/commands.d.ts +4 -1
  43. package/dist/core/audit/commands.d.ts.map +1 -1
  44. package/dist/core/audit/commands.js +7 -3
  45. package/dist/core/audit/commands.js.map +1 -1
  46. package/dist/core/audit/diff.d.ts +31 -0
  47. package/dist/core/audit/diff.d.ts.map +1 -1
  48. package/dist/core/audit/diff.js +43 -0
  49. package/dist/core/audit/diff.js.map +1 -1
  50. package/dist/core/audit/errors.d.ts +4 -0
  51. package/dist/core/audit/errors.d.ts.map +1 -0
  52. package/dist/core/audit/errors.js +4 -0
  53. package/dist/core/audit/errors.js.map +1 -0
  54. package/dist/core/audit/fix-history.d.ts.map +1 -1
  55. package/dist/core/audit/fix-history.js +3 -5
  56. package/dist/core/audit/fix-history.js.map +1 -1
  57. package/dist/core/audit/fix.d.ts.map +1 -1
  58. package/dist/core/audit/fix.js +16 -18
  59. package/dist/core/audit/fix.js.map +1 -1
  60. package/dist/core/audit/history.d.ts +8 -0
  61. package/dist/core/audit/history.d.ts.map +1 -1
  62. package/dist/core/audit/history.js +67 -5
  63. package/dist/core/audit/history.js.map +1 -1
  64. package/dist/core/audit/index.d.ts.map +1 -1
  65. package/dist/core/audit/index.js +24 -12
  66. package/dist/core/audit/index.js.map +1 -1
  67. package/dist/core/audit/listChecks.d.ts.map +1 -1
  68. package/dist/core/audit/listChecks.js +2 -1
  69. package/dist/core/audit/listChecks.js.map +1 -1
  70. package/dist/core/audit/pluginAudit.d.ts +10 -0
  71. package/dist/core/audit/pluginAudit.d.ts.map +1 -1
  72. package/dist/core/audit/pluginAudit.js +59 -23
  73. package/dist/core/audit/pluginAudit.js.map +1 -1
  74. package/dist/core/audit/pluginFix.d.ts +22 -1
  75. package/dist/core/audit/pluginFix.d.ts.map +1 -1
  76. package/dist/core/audit/pluginFix.js +27 -10
  77. package/dist/core/audit/pluginFix.js.map +1 -1
  78. package/dist/core/audit/regression.d.ts +1 -0
  79. package/dist/core/audit/regression.d.ts.map +1 -1
  80. package/dist/core/audit/regression.js +7 -5
  81. package/dist/core/audit/regression.js.map +1 -1
  82. package/dist/core/audit/snapshot.d.ts.map +1 -1
  83. package/dist/core/audit/snapshot.js +3 -4
  84. package/dist/core/audit/snapshot.js.map +1 -1
  85. package/dist/core/audit/types.d.ts +2 -2
  86. package/dist/core/audit/types.d.ts.map +1 -1
  87. package/dist/core/completions.d.ts.map +1 -1
  88. package/dist/core/completions.js +71 -47
  89. package/dist/core/completions.js.map +1 -1
  90. package/dist/core/configRepair.d.ts.map +1 -1
  91. package/dist/core/configRepair.js +5 -12
  92. package/dist/core/configRepair.js.map +1 -1
  93. package/dist/core/doctor.d.ts +2 -1
  94. package/dist/core/doctor.d.ts.map +1 -1
  95. package/dist/core/doctor.js +4 -5
  96. package/dist/core/doctor.js.map +1 -1
  97. package/dist/core/evidence.d.ts.map +1 -1
  98. package/dist/core/evidence.js +4 -7
  99. package/dist/core/evidence.js.map +1 -1
  100. package/dist/core/fleet.d.ts +5 -2
  101. package/dist/core/fleet.d.ts.map +1 -1
  102. package/dist/core/fleet.js +34 -22
  103. package/dist/core/fleet.js.map +1 -1
  104. package/dist/core/notify.d.ts.map +1 -1
  105. package/dist/core/notify.js +13 -24
  106. package/dist/core/notify.js.map +1 -1
  107. package/dist/core/plugin/audit.d.ts +25 -0
  108. package/dist/core/plugin/audit.d.ts.map +1 -0
  109. package/dist/core/plugin/audit.js +43 -0
  110. package/dist/core/plugin/audit.js.map +1 -0
  111. package/dist/core/plugin.d.ts +19 -6
  112. package/dist/core/plugin.d.ts.map +1 -1
  113. package/dist/core/plugin.js +40 -19
  114. package/dist/core/plugin.js.map +1 -1
  115. package/dist/core/provision.d.ts +25 -1
  116. package/dist/core/provision.d.ts.map +1 -1
  117. package/dist/core/provision.js +127 -12
  118. package/dist/core/provision.js.map +1 -1
  119. package/dist/core/scheduleManager.d.ts.map +1 -1
  120. package/dist/core/scheduleManager.js +7 -8
  121. package/dist/core/scheduleManager.js.map +1 -1
  122. package/dist/core/tokens.d.ts +1 -1
  123. package/dist/core/tokens.d.ts.map +1 -1
  124. package/dist/core/tokens.js +12 -11
  125. package/dist/core/tokens.js.map +1 -1
  126. package/dist/index.js +2 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/mcp/index.js +2 -2
  129. package/dist/mcp/index.js.map +1 -1
  130. package/dist/mcp/server.d.ts +14 -0
  131. package/dist/mcp/server.d.ts.map +1 -1
  132. package/dist/mcp/server.js +118 -96
  133. package/dist/mcp/server.js.map +1 -1
  134. package/dist/mcp/startupDiagnostic.d.ts +6 -0
  135. package/dist/mcp/startupDiagnostic.d.ts.map +1 -0
  136. package/dist/mcp/startupDiagnostic.js +7 -0
  137. package/dist/mcp/startupDiagnostic.js.map +1 -0
  138. package/dist/mcp/tools/serverAudit.d.ts +2 -1
  139. package/dist/mcp/tools/serverAudit.d.ts.map +1 -1
  140. package/dist/mcp/tools/serverAudit.js.map +1 -1
  141. package/dist/mcp/tools/serverBackup.handlers.d.ts.map +1 -1
  142. package/dist/mcp/tools/serverBackup.handlers.js +1 -0
  143. package/dist/mcp/tools/serverBackup.handlers.js.map +1 -1
  144. package/dist/mcp/tools/serverCompare.d.ts +13 -14
  145. package/dist/mcp/tools/serverCompare.d.ts.map +1 -1
  146. package/dist/mcp/tools/serverCompare.js +20 -15
  147. package/dist/mcp/tools/serverCompare.js.map +1 -1
  148. package/dist/mcp/tools/serverFix.d.ts +61 -17
  149. package/dist/mcp/tools/serverFix.d.ts.map +1 -1
  150. package/dist/mcp/tools/serverFix.js +67 -78
  151. package/dist/mcp/tools/serverFix.js.map +1 -1
  152. package/dist/mcp/tools/serverGuard.d.ts.map +1 -1
  153. package/dist/mcp/tools/serverGuard.js +4 -1
  154. package/dist/mcp/tools/serverGuard.js.map +1 -1
  155. package/dist/mcp/tools/serverInfo.d.ts +11 -3
  156. package/dist/mcp/tools/serverInfo.d.ts.map +1 -1
  157. package/dist/mcp/tools/serverInfo.js +11 -3
  158. package/dist/mcp/tools/serverInfo.js.map +1 -1
  159. package/dist/mcp/tools/serverLogs.d.ts.map +1 -1
  160. package/dist/mcp/tools/serverLogs.js +2 -1
  161. package/dist/mcp/tools/serverLogs.js.map +1 -1
  162. package/dist/mcp/tools/serverMaintain.d.ts +4 -2
  163. package/dist/mcp/tools/serverMaintain.d.ts.map +1 -1
  164. package/dist/mcp/tools/serverMaintain.js +4 -2
  165. package/dist/mcp/tools/serverMaintain.js.map +1 -1
  166. package/dist/mcp/tools/serverPlugin.js +2 -2
  167. package/dist/mcp/tools/serverPlugin.js.map +1 -1
  168. package/dist/mcp/tools/serverProvision.d.ts +8 -0
  169. package/dist/mcp/tools/serverProvision.d.ts.map +1 -1
  170. package/dist/mcp/tools/serverProvision.js +31 -3
  171. package/dist/mcp/tools/serverProvision.js.map +1 -1
  172. package/dist/mcp/tools/serverSecure.actions.d.ts +21 -0
  173. package/dist/mcp/tools/serverSecure.actions.d.ts.map +1 -0
  174. package/dist/mcp/tools/serverSecure.actions.js +22 -0
  175. package/dist/mcp/tools/serverSecure.actions.js.map +1 -0
  176. package/dist/mcp/tools/serverSecure.d.ts +23 -1
  177. package/dist/mcp/tools/serverSecure.d.ts.map +1 -1
  178. package/dist/mcp/tools/serverSecure.js +16 -9
  179. package/dist/mcp/tools/serverSecure.js.map +1 -1
  180. package/dist/mcp/utils/parseMetrics.d.ts +27 -0
  181. package/dist/mcp/utils/parseMetrics.d.ts.map +1 -0
  182. package/dist/mcp/utils/parseMetrics.js +35 -0
  183. package/dist/mcp/utils/parseMetrics.js.map +1 -0
  184. package/dist/mcp/utils.d.ts +9 -0
  185. package/dist/mcp/utils.d.ts.map +1 -1
  186. package/dist/mcp/utils.js +1 -2
  187. package/dist/mcp/utils.js.map +1 -1
  188. package/dist/mcp-bundle.mjs +5862 -4939
  189. package/dist/plugin/loader.js +3 -2
  190. package/dist/plugin/loader.js.map +1 -1
  191. package/dist/plugin/registry.d.ts +26 -5
  192. package/dist/plugin/registry.d.ts.map +1 -1
  193. package/dist/plugin/registry.js +46 -18
  194. package/dist/plugin/registry.js.map +1 -1
  195. package/dist/plugin/sdk/constants.d.ts +2 -0
  196. package/dist/plugin/sdk/constants.d.ts.map +1 -1
  197. package/dist/plugin/sdk/constants.js +1 -0
  198. package/dist/plugin/sdk/constants.js.map +1 -1
  199. package/dist/plugin/sdk/types.d.ts +18 -4
  200. package/dist/plugin/sdk/types.d.ts.map +1 -1
  201. package/dist/plugin/sdk/types.js +1 -1
  202. package/dist/plugin/sdk/types.js.map +1 -1
  203. package/dist/plugin/validate.d.ts.map +1 -1
  204. package/dist/plugin/validate.js +17 -8
  205. package/dist/plugin/validate.js.map +1 -1
  206. package/dist/types/severity.d.ts +3 -0
  207. package/dist/types/severity.d.ts.map +1 -0
  208. package/dist/types/severity.js +2 -0
  209. package/dist/types/severity.js.map +1 -0
  210. package/dist/utils/atomicWrite.d.ts +23 -0
  211. package/dist/utils/atomicWrite.d.ts.map +1 -0
  212. package/dist/utils/atomicWrite.js +44 -0
  213. package/dist/utils/atomicWrite.js.map +1 -0
  214. package/dist/utils/concurrency.d.ts +17 -0
  215. package/dist/utils/concurrency.d.ts.map +1 -0
  216. package/dist/utils/concurrency.js +38 -0
  217. package/dist/utils/concurrency.js.map +1 -0
  218. package/dist/utils/config.d.ts +1 -0
  219. package/dist/utils/config.d.ts.map +1 -1
  220. package/dist/utils/config.js +44 -33
  221. package/dist/utils/config.js.map +1 -1
  222. package/dist/utils/encryption.d.ts.map +1 -1
  223. package/dist/utils/encryption.js +7 -2
  224. package/dist/utils/encryption.js.map +1 -1
  225. package/dist/utils/exitCode.d.ts +2 -0
  226. package/dist/utils/exitCode.d.ts.map +1 -0
  227. package/dist/utils/exitCode.js +4 -0
  228. package/dist/utils/exitCode.js.map +1 -0
  229. package/dist/utils/fileLock.d.ts.map +1 -1
  230. package/dist/utils/fileLock.js +177 -30
  231. package/dist/utils/fileLock.js.map +1 -1
  232. package/dist/utils/fsMtime.d.ts +32 -0
  233. package/dist/utils/fsMtime.d.ts.map +1 -0
  234. package/dist/utils/fsMtime.js +61 -0
  235. package/dist/utils/fsMtime.js.map +1 -0
  236. package/dist/utils/fsRetry.d.ts +20 -0
  237. package/dist/utils/fsRetry.d.ts.map +1 -0
  238. package/dist/utils/fsRetry.js +56 -0
  239. package/dist/utils/fsRetry.js.map +1 -0
  240. package/dist/utils/logger.d.ts +1 -1
  241. package/dist/utils/logger.d.ts.map +1 -1
  242. package/dist/utils/logger.js +8 -3
  243. package/dist/utils/logger.js.map +1 -1
  244. package/dist/utils/openBrowser.d.ts.map +1 -1
  245. package/dist/utils/openBrowser.js +3 -2
  246. package/dist/utils/openBrowser.js.map +1 -1
  247. package/dist/utils/platform.d.ts +2 -0
  248. package/dist/utils/platform.d.ts.map +1 -0
  249. package/dist/utils/platform.js +2 -0
  250. package/dist/utils/platform.js.map +1 -0
  251. package/dist/utils/secureWrite.d.ts +1 -0
  252. package/dist/utils/secureWrite.d.ts.map +1 -1
  253. package/dist/utils/secureWrite.js +8 -2
  254. package/dist/utils/secureWrite.js.map +1 -1
  255. package/dist/utils/securityLogger.d.ts.map +1 -1
  256. package/dist/utils/securityLogger.js +16 -4
  257. package/dist/utils/securityLogger.js.map +1 -1
  258. package/dist/utils/ssh.d.ts +2 -1
  259. package/dist/utils/ssh.d.ts.map +1 -1
  260. package/dist/utils/ssh.js +14 -4
  261. package/dist/utils/ssh.js.map +1 -1
  262. package/dist/utils/version.d.ts +5 -0
  263. package/dist/utils/version.d.ts.map +1 -1
  264. package/dist/utils/version.js +26 -0
  265. package/dist/utils/version.js.map +1 -1
  266. package/dist/utils/webhookSecurity.d.ts +13 -0
  267. package/dist/utils/webhookSecurity.d.ts.map +1 -0
  268. package/dist/utils/webhookSecurity.js +130 -0
  269. package/dist/utils/webhookSecurity.js.map +1 -0
  270. package/kastell-plugin/.claude-plugin/plugin.json +2 -2
  271. package/kastell-plugin/README.md +6 -0
  272. package/package.json +3 -4
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kastell",
3
- "version": "2.2.5",
4
- "description": "Server security auditing, hardening, and fleet management. 457 security checks across 30 categories, CIS/PCI-DSS/HIPAA compliance, 24-step production hardening, and 17 MCP tools. Supports Hetzner, DigitalOcean, Vultr, Linode with Coolify, Dokploy, and bare VPS modes.",
3
+ "version": "2.3.0",
4
+ "description": "Server security auditing, hardening, and fleet management. 470+ security checks across 32 categories, CIS/PCI-DSS/HIPAA compliance, 24-step production hardening, and 17 MCP tools. Supports Hetzner, DigitalOcean, Vultr, Linode with Coolify, Dokploy, and bare VPS modes.",
5
5
  "author": {
6
6
  "name": "kastelldev",
7
7
  "email": "hello@omrfc.dev",
@@ -32,7 +32,9 @@
32
32
  "mcpServers": {
33
33
  "kastell": {
34
34
  "command": "node",
35
- "args": ["${CLAUDE_PLUGIN_ROOT}/dist/mcp-bundle.mjs"],
35
+ "args": [
36
+ "${CLAUDE_PLUGIN_ROOT}/dist/mcp-bundle.mjs"
37
+ ],
36
38
  "env": {
37
39
  "HETZNER_TOKEN": "${HETZNER_TOKEN}",
38
40
  "DIGITALOCEAN_TOKEN": "${DIGITALOCEAN_TOKEN}",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,118 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.3.0] - 2026-06-12
6
+
7
+ ### Added
8
+ - **`markCommandFailed()` exit code helper** — `src/utils/exitCode.ts` centralizes `process.exitCode = 1` signaling; adopted across `snapshot`, `domain`, `maintain`, `update`, `audit`, `evidence`, `fix`, `init` commands (P141)
9
+ - **`isolatedKastellEnv` Jest helper** — `tests/helpers/isolatedKastellEnv.ts` provides per-test `KASTELL_DIR` isolation with self-registering `afterEach` cleanup (P141)
10
+ - **Atomic `*.tmp` rename helpers** — `atomicWriteFileSync`, `fsRetry`, `secureAppendFileSync` for Windows-safe persistence with `EPERM`/`EACCES` retry + copy+unlink fallback (P140)
11
+ - **MCP `startupDiagnostic` helper** — `src/mcp/startupDiagnostic.ts` reports SDK + build identity at MCP boot (P141)
12
+ - **`fileLock` structured return** — `assessLockState` returns owner PID hash + `ownerHost="internal"` for safe diagnostic surfacing; stale-lock reclaim now re-acquires the lock and runs `fn()` when removal succeeds (P141)
13
+ - Plugin checks now run in parallel (cap=3, configurable via `PLUGIN_AUDIT_PARALLELISM`)
14
+ - Aggregate timeout for plugin audit (`PLUGIN_AUDIT_TOTAL_TIMEOUT_MS`, default 120s)
15
+ - `safeToParallel: false` opt-out in plugin manifest for plugins with intentional mutating checkCommands
16
+ - File-mtime cache for `getServers()` and `loadLatestAudit(ip)` with network FS guard
17
+ - MCP SDK round-trip test for serverCompare schema
18
+ - Plugin tarball smoke test: MCP boot time measurement + empty dir detection
19
+
20
+ ### Changed
21
+ - Breaking: Plugin SDK audit checks now require `apiVersion: "2"` and object-shaped `checkCommand: { kind, cmd }`. Manifest-level `mutates` and `safeToParallel` are removed; mutation intent is declared per check.
22
+ - **`chunkConcurrent`** replaces `p-limit` in fleet.ts (p-limit dependency dropped)
23
+ - `serverCompare` outputSchema uses `discriminatedUnion` (dış shape aynı kalır)
24
+ - `PluginRegistryEntry` is now a discriminated union (loaded/error/disabled)
25
+ - CI workflow: build artifact shared between jobs (~30s saving per run)
26
+ - **`fileLock` reclaim now re-acquires the lock and runs `fn()` when stale removal succeeds** — previously only recorded the reclaim error and gave up (P141 Codex gap)
27
+ - **Explicit `process.env[PROVIDER_TOKEN]` overrides buffered/keychain tokens** — prevents stale desktop keychain entries from breaking MCP/container calls (P141 Codex gap)
28
+ - **`server_info` health outputSchema** — single-server fields (`server`, `ip`, `mode`, `sshReachable`, `hostKeyMismatch`, `platformStatus`, `coolifyUrl`, `dokployUrl`) now part of the schema; `results`/`summary` optional for per-server response (P141 Codex gap)
29
+
30
+ Release-time version bumps remain owned by `/release minor`; `package.json` and `kastell-plugin/.claude-plugin/plugin.json` must be bumped together.
31
+
32
+ ### Fixed
33
+ - `serverCompare` detail mode returns flat checks array (was object — CQS-11 #1)
34
+ - `ssh-factories.ts` setTimeout leak — worker force-exit (CQS-10 #1)
35
+ - `tests/helpers/fsMock.ts` factory prevents Linux CI chmodSync mock omission
36
+ - Explicit provider token environment variables now override buffered and keychain credentials, preventing stale desktop keychain entries from breaking MCP/container calls.
37
+ - Single-server `server_info health` responses now validate against the registered MCP output schema.
38
+ - **Windows local state writes** — Atomic `*.tmp` rename persistence now retries transient `EPERM`/`EACCES` failures and falls back to copy+unlink safely. Coverage: `servers.json`, audit history, evidence manifests (`MANIFEST.json`, `SHA256SUMS`), audit snapshots, fix history, regression baselines, and metric history. File-lock reclaim and security-log rotation also retry on transient `EPERM`/`EACCES`.
39
+ - **Status command error path exits 1** via `markCommandFailed()` — partial failures now propagate non-zero exit code matching other reliability contracts (P141)
40
+ - **Audit `--json` and `--ci` stdout parse-clean** — no log pollution; reserved for a single JSON payload (P141)
41
+ - **Windows file-lock diagnostics** for persistent `EPERM`/`EACCES` — hint-rich surface, replaces "transient" claim (P141)
42
+ - **`printDiff` regression path** unconditionally marks `process.exitCode = 1` (P141 `/simplify`)
43
+
44
+ ### Internal
45
+ - 175 `console.log` triage + sweep (5 categories — 0 actionable)
46
+ - 13 slow tests audit (P140 input)
47
+ - `createFsMock` factory adopted across 32 test files
48
+ - `McpServerInternal` named type (CQS-05)
49
+ - `serverCompare` eslint-disable orphan cleanup (CQS-10 #2)
50
+
51
+ ### BREAKING
52
+ - `server_fix` MCP input shape changed: `dryRun: boolean` removed, replaced by `mode: 'dry-run' | 'live'` on the `apply` action branch. CLI users unaffected (`--dry-run` flag unchanged). MCP consumers must update calls.
53
+ - `server_fix` MCP output shape changed: non-apply actions (`history`, `rollback`, `rollback-all`, `rollback-to`) no longer carry a `dryRun` field. Previously the field held the action name as a string proxy to satisfy a misnamed discriminator; now responses are discriminated by `action` and `dryRun: boolean` appears only on `apply` responses where it semantically belongs.
54
+ - `server_secure firewall-status` MCP output: `rules` is now `z.array(z.object({port, proto, action, from}))` (object array) instead of `z.array(z.string())` (string array). SDK probe confirms: MCP SDK strips `structuredContent` on outputSchema mismatch. Hard-cut BREAKING. (F-020)
55
+
56
+ ### Fixed
57
+ - `server_secure` action `audit` added as canonical name. `secure-audit` still accepted (deprecated, removal scheduled for v2.4) (F-011)
58
+ - `server_info status` `summary.running` correctly counts running servers when either `serverStatus` (cloud provider) or `platformStatus` (Coolify/Dokploy) is "running". Previously only checked `platformStatus`, missing servers where the cloud reports running but the platform probe fails. (F-024)
59
+ - `server_guard status` returns `success: boolean` and `logTail: string[]` (line array). (F-022)
60
+ - `server_logs monitor` returns structured `metrics.{cpu,mem,disk}` objects (bytes for total/used, IEC binary) instead of validation-failing strings. CLI output unchanged. (F-019)
61
+ - `server_backup backup-list` returns `backupCount` field (F-021)
62
+ - `kastell audit` accepts `--framework <cis-level1|cis-level2|pci-dss|hipaa>` (parity with MCP) (F-016)
63
+ - Keychain decrypt warnings now deduplicate into single line with provider list (CQS-07)
64
+ - Audit `--threshold` and all early-return paths now correctly set `process.exitCode = 1` via AuditError policy (F-015)
65
+ - `kastell add --skip-verify` now respects `--mode coolify|bare` flag (F-002)
66
+ - `kastell lock --dry-run` error message clarified (F-010)
67
+ - `kastell fix --dry-run` requires `--safe` with clear error (F-012)
68
+ - `kastell fix --history` shows informative empty state (F-014)
69
+ - Bash completions synced with command registry (F-025)
70
+
71
+ ### Changed
72
+ - MCP server tool registration refactored to iterate `ALL_MCP_TOOLS` (~340 lines removed)
73
+ - Plugin audit checks now parallelize (max 4 per host) with AbortController-based aggregate timeout (CQS-06)
74
+ - Fixture `makeServerRecord` helper extracted across 10 fixtures (~100 lines saved)
75
+ - `isWindows()` helper extracts 6 inline platform checks
76
+
77
+ ### Added
78
+ - Regression test for `server_plugin list` reading from the loaded plugin registry (fix landed in v2.2.0 P134c/d; test prevents future drift, F-018)
79
+ - `AuditError` class for centralized audit error handling
80
+ - `chunkConcurrent` helper for bounded parallel work
81
+ - `PluginSeverity` / `FixTier` shared types
82
+ - `Safety Modes` section in README
83
+ - `kastell provision` alias for `init`
84
+
85
+ ### v2.3 Reliability Contracts
86
+
87
+ - **Immediate MCP durable registration** — `server_provision` returns as soon as the provider creates the server and Kastell durably persists the record. `readiness.status` may be `pending`; follow with `server_info status` or `server_info health`.
88
+ - **Verified CLI failures return non-zero** — unsupported and failed CLI operations exit with `1`; valid empty results and user cancellation exit with `0`. Mixed `--all` failures exit with `1`.
89
+ - **Parse-clean audit stdout** — `audit --json` and `audit --ci` reserve stdout for a single JSON payload.
90
+ - **Actionable Windows lock diagnostics** — persistent `EPERM`/`EACCES` failures on Windows file-lock paths now surface hint-rich diagnostics.
91
+ - **MCP SDK round-trip coverage** — `server_manage add/remove/destroy` outputSchemas now have full `normalizeObjectSchema` + `safeParseAsync` round-trip tests.
92
+
93
+ ### Security
94
+
95
+ - **Notify webhook SSRF hardening (HIGH-001)** — Discord/Slack webhook connections now reject private/reserved IPv4 and IPv6 targets during the actual socket DNS lookup, pin connections to validated public answers, and disable redirects and environment proxies.
96
+ - **Reclassified P142 security follow-ups (reviewed 2026-06-12):**
97
+ - **MEDIUM** — `starter` CLI template skips the optional extra SSH hardening step. Platform cloud-init still configures firewall rules; bare mode also installs fail2ban and unattended-upgrades.
98
+ - **MEDIUM** — SSH `StrictHostKeyChecking=accept-new` carries first-connection TOFU risk; strict host-key handling needs separate policies for interactive SSH and newly provisioned servers.
99
+ - **LOW** — `debugLog` does not detect raw secret-shaped strings, though current core error-object call sites are collapsed to `[object]`.
100
+ - **LOW / operational** — CLI safe mode defaults off for trusted local operation; non-TTY automation can still benefit from safer defaults or warnings.
101
+ - **Removed from the security backlog** — Ubuntu 24.04 provider pinning is a provisioning reliability policy, not a security vulnerability; the previously documented DigitalOcean SSH issue has dedicated cloud-init mitigations.
102
+
103
+ ## [Unreleased]
104
+
105
+ ## [2.2.7] - 2026-05-16
106
+
107
+ ### Fixed
108
+ - **npm tarball plugin.json version sync** — v2.2.6 npm tarball shipped with `package.json` 2.2.6 but `.claude-plugin/plugin.json` stuck at 2.2.5; CC marketplace `/plugin update` showed correct version on disk but plugin manifest reported stale. Release flow now syncs `plugin.json` **before** `npm version` and validates tarball contents **before** push (FATAL gate). Users now see correct version after `/plugin update`.
109
+
110
+ ### Added
111
+ - **Plugin tarball smoke test (`scripts/smoke-plugin-install.sh`)** — simulates CC plugin install (no `npm install`): runs `npm pack`, extracts tarball, verifies all manifest paths shipped, and boots MCP bundle without module errors
112
+ - **CI `plugin-manifest` job** — schema validation + version drift detection + smoke test on Ubuntu/Node 20 (catches plugin shipping issues before publish)
113
+
114
+ ### Changed
115
+ - **Test mock race fix** — `process.nextTick` replaces `setTimeout(_, 5)` for stderr emit in `mockProcess.ts`, `mcp-server-backup.test.ts`, `restore.test.ts`; eliminates flaky `scpDownload` timing race on macOS-Node20 CI runners (5ms stderr vs 10ms close ordering)
116
+
5
117
  ## [2.2.6] - 2026-05-16
6
118
 
7
119
  ### Added
package/README.md CHANGED
@@ -46,7 +46,7 @@ Running `kastell` without any arguments launches an **interactive search menu**
46
46
  ██║ ██╗ ██║ ██║ ███████║ ██║ ███████╗███████╗███████╗
47
47
  ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚══════╝╚══════╝
48
48
 
49
- KASTELL v2.2.6 · Your infrastructure, fortified.
49
+ KASTELL v2.3.0 · Your infrastructure, fortified.
50
50
 
51
51
  $ kastell init --template production → deploy a new server
52
52
  $ kastell status --all → check all servers
@@ -95,9 +95,9 @@ Kastell handles server provisioning, SSH key setup, firewall configuration, and
95
95
  |---------|---------|-------|----------|
96
96
  | Installation | `npm i -g kastell` | Package manager | Package manager |
97
97
  | Language | TypeScript | Shell | C/Python |
98
- | Security Checks | 457+ | 300+ | Varies by profile |
98
+ | Security Checks | 470+ | 300+ | Varies by profile |
99
99
  | Auto-Fix | Safe tier | Suggest only | Suggest only |
100
- | MCP (AI Agent) | 14 tools | -- | -- |
100
+ | MCP (AI Agent) | 17 tools | -- | -- |
101
101
  | Compliance | CIS, PCI-DSS, HIPAA | CIS, HIPAA | CIS, STIG, PCI-DSS |
102
102
  | Cloud Provision | 4 providers | -- | -- |
103
103
  | Hardening (Lock) | 24-step | -- | -- |
@@ -170,7 +170,7 @@ kastell domain add my-server --domain example.com # Set domain + SSL
170
170
 
171
171
  ### Security Audit
172
172
  ```bash
173
- kastell audit my-server # Full security audit (31 categories, 468+ checks)
173
+ kastell audit my-server # Full security audit (32 categories, 470+ checks)
174
174
  kastell audit my-server --json # JSON output for automation
175
175
  kastell audit my-server --threshold 70 # Exit code 1 if score below threshold
176
176
  kastell audit my-server --fix # Interactive fix mode (prompts per severity)
@@ -263,7 +263,7 @@ kastell init --template production --provider hetzner
263
263
 
264
264
  ## Security
265
265
 
266
- Kastell is built with security as a priority -- **9,871 tests** across 219 suites, including dedicated security test suites.
266
+ Kastell is built with security as a priority -- **11,206 tests** across 344 suites, including dedicated security test suites.
267
267
 
268
268
  - API tokens are never stored on disk -- prompted at runtime or via environment variables
269
269
  - SSH keys are auto-generated if needed (Ed25519)
@@ -291,6 +291,16 @@ kastell <command>
291
291
 
292
292
  Requires Node.js 20 or later.
293
293
 
294
+ ## Safety Modes
295
+
296
+ `KASTELL_SAFE_MODE` environment variable controls destructive operations:
297
+
298
+ - **MCP default:** `true` — `provision`, `destroy`, `restore` are blocked
299
+ - **CLI default:** `false` — all operations are enabled
300
+ - **Override:** `KASTELL_SAFE_MODE=true kastell destroy <server>` → rejected
301
+
302
+ Affected commands: `init`, `destroy`, `backup-restore`, `snapshot-restore`, `snapshot-delete`, `restart`, `maintain`.
303
+
294
304
  ## Troubleshooting
295
305
 
296
306
  **Server creation fails?**
@@ -306,7 +316,7 @@ Use `kastell status my-server --autostart` to check platform status and auto-res
306
316
 
307
317
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and contribution guidelines.
308
318
 
309
- Kastell uses **9,871 tests** across 219 suites. Run `npm test` before submitting PRs.
319
+ Kastell uses **11,206 tests** across 344 suites. Run `npm test` before submitting PRs.
310
320
 
311
321
  ## MCP Server (AI Integration)
312
322
 
@@ -375,6 +385,24 @@ Install via Claude Code plugin manager or use directly with `claude --plugin-dir
375
385
 
376
386
  Kastell provides [`llms.txt`](llms.txt) for AI crawlers and is listed in the [MCP Registry](https://registry.modelcontextprotocol.io/) as `io.github.kastelldev/kastell`.
377
387
 
388
+ ## v2.3 Reliability Contracts
389
+
390
+ These contracts apply to the CLI and the MCP server.
391
+
392
+ ### Provisioning behavior
393
+
394
+ `server_provision` returns after the provider creates the server and Kastell
395
+ durably registers it. `readiness.status` may be `pending`; follow with
396
+ `server_info status` or `server_info health`. The interactive `kastell init`
397
+ command continues waiting through its existing readiness checks.
398
+
399
+ ### Automation contracts
400
+
401
+ - Unsupported and failed CLI operations return exit code `1`.
402
+ - Valid empty results and user cancellation return `0`.
403
+ - Mixed `--all` failures return `1`.
404
+ - `audit --json` and `audit --ci` reserve stdout for one JSON payload.
405
+
378
406
  ## CI/CD Integration
379
407
 
380
408
  Use `kastell audit` in your CI pipeline to enforce security baselines:
package/README.tr.md CHANGED
@@ -46,7 +46,7 @@ npx kastell
46
46
  ██║ ██╗ ██║ ██║ ███████║ ██║ ███████╗███████╗███████╗
47
47
  ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚══════╝╚══════╝
48
48
 
49
- KASTELL v2.2.4 · Your infrastructure, fortified.
49
+ KASTELL v2.3.0 · Your infrastructure, fortified.
50
50
 
51
51
  $ kastell init --template production → deploy a new server
52
52
  $ kastell status --all → check all servers
@@ -95,9 +95,9 @@ Kastell sunucu oluşturma, SSH anahtar kurulumu, güvenlik duvarı yapılandırm
95
95
  |---------|---------|-------|----------|
96
96
  | Kurulum | `npm i -g kastell` | Paket yoneticisi | Paket yoneticisi |
97
97
  | Dil | TypeScript | Shell | C/Python |
98
- | Guvenlik Kontrolleri | 457+ | 300+ | Profile gore degisir |
98
+ | Guvenlik Kontrolleri | 470+ | 300+ | Profile gore degisir |
99
99
  | Otomatik Duzeltme | Guvenli katman | Sadece oneri | Sadece oneri |
100
- | MCP (AI Ajan) | 14 arac | -- | -- |
100
+ | MCP (AI Ajan) | 17 arac | -- | -- |
101
101
  | Uyumluluk | CIS, PCI-DSS, HIPAA | CIS, HIPAA | CIS, STIG, PCI-DSS |
102
102
  | Bulut Saglama | 4 saglayici | -- | -- |
103
103
  | Siklastirma (Lock) | 24 adim | -- | -- |
@@ -170,7 +170,7 @@ kastell domain add sunucum --domain ornek.com # Domain + SSL ayarla
170
170
 
171
171
  ### Güvenlik Denetimi
172
172
  ```bash
173
- kastell audit sunucum # Tam güvenlik denetimi (31 kategori, 468+ kontrol)
173
+ kastell audit sunucum # Tam güvenlik denetimi (32 kategori, 470+ kontrol)
174
174
  kastell audit sunucum --json # Otomasyon için JSON çıktısı
175
175
  kastell audit sunucum --threshold 70 # Skor eşiğin altındaysa exit code 1
176
176
  kastell audit sunucum --fix # İnteraktif düzeltme modu (önem derecesine göre)
@@ -263,7 +263,7 @@ kastell init --template production --provider hetzner
263
263
 
264
264
  ## Güvenlik
265
265
 
266
- Kastell güvenlik öncelikli olarak geliştirilmektedir -- 219 test suite'inde **9.871 test**, özel güvenlik test suite'leri dahil.
266
+ Kastell güvenlik öncelikli olarak geliştirilmektedir -- 344 test suite'inde **11.206 test**, özel güvenlik test suite'leri dahil.
267
267
 
268
268
  - API token'ları asla diske kaydedilmez -- çalışma zamanında sorulur veya ortam değişkenlerinden alınır
269
269
  - SSH anahtarları gerekirse otomatik oluşturulur (Ed25519)
@@ -306,7 +306,7 @@ Platform durumunu kontrol edip gerekirse otomatik yeniden başlatmak için `kast
306
306
 
307
307
  Geliştirme ortamı kurulumu, test ve katkı rehberi için [CONTRIBUTING.md](CONTRIBUTING.md) dosyasına bakın.
308
308
 
309
- Kastell, 207 suite'te **6.441 test** kullanmaktadır. PR göndermeden önce `npm test` çalıştırın.
309
+ Kastell, 344 suite'te **11.206 test** kullanmaktadır. PR göndermeden önce `npm test` çalıştırın.
310
310
 
311
311
  ## MCP Sunucusu (Yapay Zeka Entegrasyonu)
312
312
 
@@ -19,14 +19,15 @@ export interface AuditCommandOptions extends AuditCliOptions {
19
19
  days?: string;
20
20
  listChecks?: boolean;
21
21
  profile?: string;
22
+ framework?: string;
22
23
  compliance?: string;
23
24
  fresh?: boolean;
24
25
  detail?: boolean;
25
26
  ci?: boolean;
26
27
  }
27
28
  /**
28
- * Execute the audit command.
29
- * Flow: resolveServer (or parse --host) -> runAudit -> select formatter -> output -> threshold check
29
+ * Wrapper: catches AuditError and sets exitCode = 1.
30
+ * All early-return paths in auditCommandImpl throw AuditError instead.
30
31
  */
31
32
  export declare function auditCommand(serverName?: string, options?: AuditCommandOptions): Promise<void>;
32
33
  //# sourceMappingURL=audit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoBH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAczE,MAAM,WAAW,mBAAoB,SAAQ,eAAe;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAiXf"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqBH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAgBzE,MAAM,WAAW,mBAAoB,SAAQ,eAAe;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -22,17 +22,54 @@ import { FRAMEWORK_KEY_MAP } from "../core/audit/compliance/types.js";
22
22
  import { filterAuditResult, buildFilterAnnotation, parseSeverity } from "../core/audit/filter.js";
23
23
  import { saveBaselineSafe, loadBaseline, checkRegression, formatRegressionSummary, extractPassedCheckIds, shouldUpdateBaseline } from "../core/audit/regression.js";
24
24
  import { loadDefaults } from "../core/defaults.js";
25
+ import { AuditError } from "../core/audit/errors.js";
26
+ import { markCommandFailed } from "../utils/exitCode.js";
25
27
  function printDiff(diff, json) {
26
28
  console.log(json ? formatDiffJson(diff) : formatDiffTerminal(diff));
27
29
  if (diff.regressions.length > 0) {
28
- process.exitCode = 1;
30
+ markCommandFailed();
31
+ }
32
+ }
33
+ /**
34
+ * Wrapper: catches AuditError and sets exitCode = 1.
35
+ * All early-return paths in auditCommandImpl throw AuditError instead.
36
+ */
37
+ export async function auditCommand(serverName, options = {}) {
38
+ try {
39
+ await auditCommandImpl(serverName, options);
40
+ }
41
+ catch (err) {
42
+ if (err instanceof AuditError) {
43
+ logger.error(err.message);
44
+ markCommandFailed();
45
+ return;
46
+ }
47
+ throw err;
29
48
  }
30
49
  }
31
50
  /**
32
51
  * Execute the audit command.
33
52
  * Flow: resolveServer (or parse --host) -> runAudit -> select formatter -> output -> threshold check
34
53
  */
35
- export async function auditCommand(serverName, options = {}) {
54
+ async function auditCommandImpl(serverName, options = {}) {
55
+ if (options.ci) {
56
+ options.json = true;
57
+ }
58
+ const machineOutput = options.json === true || options.ci === true;
59
+ const logDiagnostic = (severity, message) => {
60
+ if (machineOutput) {
61
+ process.stderr.write(`${message}\n`);
62
+ }
63
+ else if (severity === "warning") {
64
+ logger.warning(message);
65
+ }
66
+ else if (severity === "success") {
67
+ logger.success(message);
68
+ }
69
+ else {
70
+ logger.info(message);
71
+ }
72
+ };
36
73
  // --list-checks: static catalog display — no SSH connection needed
37
74
  if (options.listChecks) {
38
75
  const filter = {};
@@ -62,12 +99,8 @@ export async function auditCommand(serverName, options = {}) {
62
99
  }
63
100
  }
64
101
  // --ci mode: validate threshold requirement early (before server resolution)
65
- if (options.ci) {
66
- if (options.threshold === undefined) {
67
- logger.error("--ci requires --threshold (e.g. --ci --threshold 70)");
68
- return;
69
- }
70
- options.json = true;
102
+ if (options.ci && options.threshold === undefined) {
103
+ throw new AuditError("--ci requires --threshold (e.g. --ci --threshold 70)");
71
104
  }
72
105
  let ip;
73
106
  let name;
@@ -130,19 +163,16 @@ export async function auditCommand(serverName, options = {}) {
130
163
  if (options.diff) {
131
164
  const parts = options.diff.split(":");
132
165
  if (parts.length !== 2) {
133
- logger.error("--diff requires format: before:after (e.g. pre-upgrade:latest)");
134
- return;
166
+ throw new AuditError("--diff requires format: before:after (e.g. pre-upgrade:latest)");
135
167
  }
136
168
  const [beforeRef, afterRef] = parts;
137
169
  const beforeSnap = await resolveSnapshotRef(ip, beforeRef);
138
170
  const afterSnap = await resolveSnapshotRef(ip, afterRef);
139
171
  if (!beforeSnap) {
140
- logger.error(`Snapshot not found: ${beforeRef}`);
141
- return;
172
+ throw new AuditError(`Snapshot not found: ${beforeRef}`);
142
173
  }
143
174
  if (!afterSnap) {
144
- logger.error(`Snapshot not found: ${afterRef}`);
145
- return;
175
+ throw new AuditError(`Snapshot not found: ${afterRef}`);
146
176
  }
147
177
  const diff = diffAudits(beforeSnap.audit, afterSnap.audit, {
148
178
  before: beforeSnap.name ?? beforeRef,
@@ -155,28 +185,24 @@ export async function auditCommand(serverName, options = {}) {
155
185
  if (options.compare) {
156
186
  const parts = options.compare.split(":");
157
187
  if (parts.length !== 2) {
158
- logger.error("--compare requires format: server1:server2");
159
- return;
188
+ throw new AuditError("--compare requires format: server1:server2");
160
189
  }
161
190
  const [serverARef, serverBRef] = parts;
162
191
  const servers = getServers();
163
192
  const serverA = servers.find((s) => s.name === serverARef || s.ip === serverARef);
164
193
  const serverB = servers.find((s) => s.name === serverBRef || s.ip === serverBRef);
165
194
  if (!serverA) {
166
- logger.error(`Server not found: ${serverARef}`);
167
- return;
195
+ throw new AuditError(`Server not found: ${serverARef}`);
168
196
  }
169
197
  if (!serverB) {
170
- logger.error(`Server not found: ${serverBRef}`);
171
- return;
198
+ throw new AuditError(`Server not found: ${serverBRef}`);
172
199
  }
173
200
  const spinner = createSpinner("Comparing servers...");
174
201
  spinner.start();
175
202
  const pairResult = await resolveAuditPair(serverA, serverB, !!options.fresh);
176
203
  spinner.stop();
177
204
  if (!pairResult.success) {
178
- logger.error(pairResult.error ?? "Compare failed");
179
- return;
205
+ throw new AuditError(pairResult.error ?? "Compare failed");
180
206
  }
181
207
  const { auditA, auditB } = pairResult.data;
182
208
  if (options.detail) {
@@ -193,8 +219,7 @@ export async function auditCommand(serverName, options = {}) {
193
219
  if (options.watch !== undefined) {
194
220
  const interval = options.watch ? parseInt(options.watch, 10) : undefined;
195
221
  if (interval !== undefined && (isNaN(interval) || interval < 1)) {
196
- logger.error("Watch interval must be a positive number (seconds)");
197
- return;
222
+ throw new AuditError("Watch interval must be a positive number (seconds)");
198
223
  }
199
224
  const formatter = await selectFormatter(options);
200
225
  logger.info(`Starting watch mode for ${name} (interval: ${interval ?? 300}s)`);
@@ -204,13 +229,13 @@ export async function auditCommand(serverName, options = {}) {
204
229
  });
205
230
  return;
206
231
  }
207
- const spinner = options.ci ? null : createSpinner(`Running security audit on ${name}...`);
232
+ const spinner = machineOutput ? null : createSpinner(`Running security audit on ${name}...`);
208
233
  spinner?.start();
209
234
  const result = await runAudit(ip, name, platform);
210
235
  if (!result.success || !result.data) {
211
236
  spinner?.fail(result.error ?? "Audit failed");
212
237
  if (result.hint) {
213
- logger.info(result.hint);
238
+ logDiagnostic("info", result.hint);
214
239
  }
215
240
  return;
216
241
  }
@@ -222,10 +247,10 @@ export async function auditCommand(serverName, options = {}) {
222
247
  // Save to history (after trend detection)
223
248
  await saveAuditHistory(auditResult);
224
249
  if (trend === "methodology-change") {
225
- logger.warning("Score methodology updated. New baseline established.");
250
+ logDiagnostic("warning", "Score methodology updated. New baseline established.");
226
251
  }
227
252
  else if (trend !== "first audit") {
228
- logger.info(`Trend: ${trend}`);
253
+ logDiagnostic("info", `Trend: ${trend}`);
229
254
  }
230
255
  const baseline = loadBaseline(auditResult.serverIp);
231
256
  const passedIds = extractPassedCheckIds(auditResult);
@@ -238,10 +263,7 @@ export async function auditCommand(serverName, options = {}) {
238
263
  }
239
264
  if (regression) {
240
265
  for (const line of formatRegressionSummary(regression)) {
241
- if (line.severity === "warning")
242
- logger.warning(line.text);
243
- else
244
- logger.info(line.text);
266
+ logDiagnostic(line.severity, line.text);
245
267
  }
246
268
  }
247
269
  // --compliance: detailed Framework>Control>Check grouped report
@@ -251,8 +273,7 @@ export async function auditCommand(serverName, options = {}) {
251
273
  .map((f) => FRAMEWORK_KEY_MAP[f.trim().toLowerCase()])
252
274
  .filter((f) => !!f);
253
275
  if (frameworks.length === 0) {
254
- logger.error("Invalid framework. Use: cis, pci-dss, hipaa");
255
- return;
276
+ throw new AuditError("Invalid framework. Use: cis, pci-dss, hipaa");
256
277
  }
257
278
  if (options.json) {
258
279
  const detail = calculateComplianceDetail(auditResult.categories);
@@ -264,12 +285,34 @@ export async function auditCommand(serverName, options = {}) {
264
285
  }
265
286
  return;
266
287
  }
288
+ // --framework: filtered audit view by single compliance framework
289
+ if (options.framework) {
290
+ const validFrameworks = ["cis-level1", "cis-level2", "pci-dss", "hipaa"];
291
+ if (!validFrameworks.includes(options.framework)) {
292
+ throw new AuditError(`Invalid framework: ${options.framework}. Valid: ${validFrameworks.join(", ")}`);
293
+ }
294
+ const fw = FRAMEWORK_KEY_MAP[options.framework];
295
+ const filteredResult = filterByProfile(auditResult, options.framework);
296
+ const detail = calculateComplianceDetail(auditResult.categories);
297
+ const fwScore = detail.find((d) => d.framework === fw);
298
+ if (options.json) {
299
+ const fwDetail = detail.filter((d) => d.framework === fw);
300
+ console.log(JSON.stringify({ overallScore: auditResult.overallScore, compliance: fwDetail }, null, 2));
301
+ }
302
+ else {
303
+ const formatter = await selectFormatter(options);
304
+ console.log(formatter(filteredResult));
305
+ if (fwScore) {
306
+ logger.info(`Framework ${options.framework}: ${fwScore.passedControls}/${fwScore.totalControls} controls (${fwScore.passRate}%)`);
307
+ }
308
+ }
309
+ return;
310
+ }
267
311
  // --profile: filtered audit view by compliance framework
268
312
  if (options.profile) {
269
313
  const validProfiles = ["cis-level1", "cis-level2", "pci-dss", "hipaa"];
270
314
  if (!validProfiles.includes(options.profile)) {
271
- logger.error(`Invalid profile. Use: ${validProfiles.join(", ")}`);
272
- return;
315
+ throw new AuditError(`Invalid profile. Use: ${validProfiles.join(", ")}`);
273
316
  }
274
317
  const profileName = options.profile;
275
318
  const filteredResult = filterByProfile(auditResult, profileName);
@@ -281,7 +324,8 @@ export async function auditCommand(serverName, options = {}) {
281
324
  const detail = calculateComplianceDetail(auditResult.categories);
282
325
  const profileScore = detail.find((d) => d.framework === profileFramework);
283
326
  if (profileScore) {
284
- logger.info(`Profile ${options.profile}: ${profileScore.passedControls}/${profileScore.totalControls} controls (${profileScore.passRate}%)`);
327
+ const profileLine = `Profile ${options.profile}: ${profileScore.passedControls}/${profileScore.totalControls} controls (${profileScore.passRate}%)`;
328
+ logDiagnostic("info", profileLine);
285
329
  }
286
330
  return;
287
331
  }
@@ -289,7 +333,7 @@ export async function auditCommand(serverName, options = {}) {
289
333
  if (options.snapshot !== undefined) {
290
334
  const snapshotName = typeof options.snapshot === "string" ? options.snapshot : undefined;
291
335
  await saveSnapshot(auditResult, snapshotName);
292
- logger.success(`Snapshot saved for ${name}`);
336
+ logDiagnostic("success", `Snapshot saved for ${name}`);
293
337
  }
294
338
  // Apply display-only filter (AUX-01, AUX-02, AUX-03)
295
339
  // MUST be after saveAuditHistory + saveSnapshot to preserve unfiltered data (AUX-04)
@@ -349,11 +393,10 @@ export async function auditCommand(serverName, options = {}) {
349
393
  if (options.threshold) {
350
394
  const threshold = parseInt(options.threshold, 10);
351
395
  if (isNaN(threshold)) {
352
- logger.error("--threshold must be a number");
353
- return;
396
+ throw new AuditError("--threshold must be a number");
354
397
  }
355
398
  if (auditResult.overallScore < threshold) {
356
- process.exitCode = 1;
399
+ markCommandFailed();
357
400
  return;
358
401
  }
359
402
  }
@@ -364,11 +407,11 @@ export async function auditCommand(serverName, options = {}) {
364
407
  const output = formatter(displayResult);
365
408
  console.log(output);
366
409
  // Show filter annotation when active
367
- if (filterAnnotation) {
410
+ if (filterAnnotation && !machineOutput) {
368
411
  logger.info(`Score: ${auditResult.overallScore}/100${filterAnnotation}`);
369
412
  }
370
413
  // Show quick wins in terminal output
371
- if (auditResult.quickWins.length > 0 && !options.json && !options.badge && !options.report) {
414
+ if (auditResult.quickWins.length > 0 && !machineOutput && !options.badge && !options.report) {
372
415
  const lastWin = auditResult.quickWins[auditResult.quickWins.length - 1];
373
416
  logger.info(`Quick wins: ${auditResult.quickWins.length} fix(es) to reach ${lastWin.projectedScore}/100`);
374
417
  }
@@ -376,12 +419,10 @@ export async function auditCommand(serverName, options = {}) {
376
419
  if (options.threshold) {
377
420
  const threshold = parseInt(options.threshold, 10);
378
421
  if (isNaN(threshold)) {
379
- logger.error("--threshold must be a number");
380
- return;
422
+ throw new AuditError("--threshold must be a number");
381
423
  }
382
424
  if (auditResult.overallScore < threshold) {
383
- logger.error(`Score ${auditResult.overallScore} is below threshold ${threshold}`);
384
- process.exitCode = 1;
425
+ markCommandFailed();
385
426
  return;
386
427
  }
387
428
  }