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.
- package/.claude-plugin/plugin.json +5 -3
- package/CHANGELOG.md +112 -0
- package/README.md +34 -6
- package/README.tr.md +6 -6
- package/dist/commands/audit.d.ts +3 -2
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +88 -47
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +14 -1
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/domain.d.ts.map +1 -1
- package/dist/commands/domain.js +18 -1
- package/dist/commands/domain.js.map +1 -1
- package/dist/commands/evidence.d.ts.map +1 -1
- package/dist/commands/evidence.js +2 -1
- package/dist/commands/evidence.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +12 -4
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lock.d.ts.map +1 -1
- package/dist/commands/lock.js +7 -2
- package/dist/commands/lock.js.map +1 -1
- package/dist/commands/maintain.d.ts.map +1 -1
- package/dist/commands/maintain.js +13 -0
- package/dist/commands/maintain.js.map +1 -1
- package/dist/commands/plugin.d.ts.map +1 -1
- package/dist/commands/plugin.js +5 -4
- package/dist/commands/plugin.js.map +1 -1
- package/dist/commands/snapshot.d.ts.map +1 -1
- package/dist/commands/snapshot.js +16 -0
- package/dist/commands/snapshot.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +30 -10
- package/dist/commands/update.js.map +1 -1
- package/dist/core/audit/commands.d.ts +4 -1
- package/dist/core/audit/commands.d.ts.map +1 -1
- package/dist/core/audit/commands.js +7 -3
- package/dist/core/audit/commands.js.map +1 -1
- package/dist/core/audit/diff.d.ts +31 -0
- package/dist/core/audit/diff.d.ts.map +1 -1
- package/dist/core/audit/diff.js +43 -0
- package/dist/core/audit/diff.js.map +1 -1
- package/dist/core/audit/errors.d.ts +4 -0
- package/dist/core/audit/errors.d.ts.map +1 -0
- package/dist/core/audit/errors.js +4 -0
- package/dist/core/audit/errors.js.map +1 -0
- package/dist/core/audit/fix-history.d.ts.map +1 -1
- package/dist/core/audit/fix-history.js +3 -5
- package/dist/core/audit/fix-history.js.map +1 -1
- package/dist/core/audit/fix.d.ts.map +1 -1
- package/dist/core/audit/fix.js +16 -18
- package/dist/core/audit/fix.js.map +1 -1
- package/dist/core/audit/history.d.ts +8 -0
- package/dist/core/audit/history.d.ts.map +1 -1
- package/dist/core/audit/history.js +67 -5
- package/dist/core/audit/history.js.map +1 -1
- package/dist/core/audit/index.d.ts.map +1 -1
- package/dist/core/audit/index.js +24 -12
- package/dist/core/audit/index.js.map +1 -1
- package/dist/core/audit/listChecks.d.ts.map +1 -1
- package/dist/core/audit/listChecks.js +2 -1
- package/dist/core/audit/listChecks.js.map +1 -1
- package/dist/core/audit/pluginAudit.d.ts +10 -0
- package/dist/core/audit/pluginAudit.d.ts.map +1 -1
- package/dist/core/audit/pluginAudit.js +59 -23
- package/dist/core/audit/pluginAudit.js.map +1 -1
- package/dist/core/audit/pluginFix.d.ts +22 -1
- package/dist/core/audit/pluginFix.d.ts.map +1 -1
- package/dist/core/audit/pluginFix.js +27 -10
- package/dist/core/audit/pluginFix.js.map +1 -1
- package/dist/core/audit/regression.d.ts +1 -0
- package/dist/core/audit/regression.d.ts.map +1 -1
- package/dist/core/audit/regression.js +7 -5
- package/dist/core/audit/regression.js.map +1 -1
- package/dist/core/audit/snapshot.d.ts.map +1 -1
- package/dist/core/audit/snapshot.js +3 -4
- package/dist/core/audit/snapshot.js.map +1 -1
- package/dist/core/audit/types.d.ts +2 -2
- package/dist/core/audit/types.d.ts.map +1 -1
- package/dist/core/completions.d.ts.map +1 -1
- package/dist/core/completions.js +71 -47
- package/dist/core/completions.js.map +1 -1
- package/dist/core/configRepair.d.ts.map +1 -1
- package/dist/core/configRepair.js +5 -12
- package/dist/core/configRepair.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +4 -5
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/evidence.d.ts.map +1 -1
- package/dist/core/evidence.js +4 -7
- package/dist/core/evidence.js.map +1 -1
- package/dist/core/fleet.d.ts +5 -2
- package/dist/core/fleet.d.ts.map +1 -1
- package/dist/core/fleet.js +34 -22
- package/dist/core/fleet.js.map +1 -1
- package/dist/core/notify.d.ts.map +1 -1
- package/dist/core/notify.js +13 -24
- package/dist/core/notify.js.map +1 -1
- package/dist/core/plugin/audit.d.ts +25 -0
- package/dist/core/plugin/audit.d.ts.map +1 -0
- package/dist/core/plugin/audit.js +43 -0
- package/dist/core/plugin/audit.js.map +1 -0
- package/dist/core/plugin.d.ts +19 -6
- package/dist/core/plugin.d.ts.map +1 -1
- package/dist/core/plugin.js +40 -19
- package/dist/core/plugin.js.map +1 -1
- package/dist/core/provision.d.ts +25 -1
- package/dist/core/provision.d.ts.map +1 -1
- package/dist/core/provision.js +127 -12
- package/dist/core/provision.js.map +1 -1
- package/dist/core/scheduleManager.d.ts.map +1 -1
- package/dist/core/scheduleManager.js +7 -8
- package/dist/core/scheduleManager.js.map +1 -1
- package/dist/core/tokens.d.ts +1 -1
- package/dist/core/tokens.d.ts.map +1 -1
- package/dist/core/tokens.js +12 -11
- package/dist/core/tokens.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +14 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +118 -96
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/startupDiagnostic.d.ts +6 -0
- package/dist/mcp/startupDiagnostic.d.ts.map +1 -0
- package/dist/mcp/startupDiagnostic.js +7 -0
- package/dist/mcp/startupDiagnostic.js.map +1 -0
- package/dist/mcp/tools/serverAudit.d.ts +2 -1
- package/dist/mcp/tools/serverAudit.d.ts.map +1 -1
- package/dist/mcp/tools/serverAudit.js.map +1 -1
- package/dist/mcp/tools/serverBackup.handlers.d.ts.map +1 -1
- package/dist/mcp/tools/serverBackup.handlers.js +1 -0
- package/dist/mcp/tools/serverBackup.handlers.js.map +1 -1
- package/dist/mcp/tools/serverCompare.d.ts +13 -14
- package/dist/mcp/tools/serverCompare.d.ts.map +1 -1
- package/dist/mcp/tools/serverCompare.js +20 -15
- package/dist/mcp/tools/serverCompare.js.map +1 -1
- package/dist/mcp/tools/serverFix.d.ts +61 -17
- package/dist/mcp/tools/serverFix.d.ts.map +1 -1
- package/dist/mcp/tools/serverFix.js +67 -78
- package/dist/mcp/tools/serverFix.js.map +1 -1
- package/dist/mcp/tools/serverGuard.d.ts.map +1 -1
- package/dist/mcp/tools/serverGuard.js +4 -1
- package/dist/mcp/tools/serverGuard.js.map +1 -1
- package/dist/mcp/tools/serverInfo.d.ts +11 -3
- package/dist/mcp/tools/serverInfo.d.ts.map +1 -1
- package/dist/mcp/tools/serverInfo.js +11 -3
- package/dist/mcp/tools/serverInfo.js.map +1 -1
- package/dist/mcp/tools/serverLogs.d.ts.map +1 -1
- package/dist/mcp/tools/serverLogs.js +2 -1
- package/dist/mcp/tools/serverLogs.js.map +1 -1
- package/dist/mcp/tools/serverMaintain.d.ts +4 -2
- package/dist/mcp/tools/serverMaintain.d.ts.map +1 -1
- package/dist/mcp/tools/serverMaintain.js +4 -2
- package/dist/mcp/tools/serverMaintain.js.map +1 -1
- package/dist/mcp/tools/serverPlugin.js +2 -2
- package/dist/mcp/tools/serverPlugin.js.map +1 -1
- package/dist/mcp/tools/serverProvision.d.ts +8 -0
- package/dist/mcp/tools/serverProvision.d.ts.map +1 -1
- package/dist/mcp/tools/serverProvision.js +31 -3
- package/dist/mcp/tools/serverProvision.js.map +1 -1
- package/dist/mcp/tools/serverSecure.actions.d.ts +21 -0
- package/dist/mcp/tools/serverSecure.actions.d.ts.map +1 -0
- package/dist/mcp/tools/serverSecure.actions.js +22 -0
- package/dist/mcp/tools/serverSecure.actions.js.map +1 -0
- package/dist/mcp/tools/serverSecure.d.ts +23 -1
- package/dist/mcp/tools/serverSecure.d.ts.map +1 -1
- package/dist/mcp/tools/serverSecure.js +16 -9
- package/dist/mcp/tools/serverSecure.js.map +1 -1
- package/dist/mcp/utils/parseMetrics.d.ts +27 -0
- package/dist/mcp/utils/parseMetrics.d.ts.map +1 -0
- package/dist/mcp/utils/parseMetrics.js +35 -0
- package/dist/mcp/utils/parseMetrics.js.map +1 -0
- package/dist/mcp/utils.d.ts +9 -0
- package/dist/mcp/utils.d.ts.map +1 -1
- package/dist/mcp/utils.js +1 -2
- package/dist/mcp/utils.js.map +1 -1
- package/dist/mcp-bundle.mjs +5862 -4939
- package/dist/plugin/loader.js +3 -2
- package/dist/plugin/loader.js.map +1 -1
- package/dist/plugin/registry.d.ts +26 -5
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +46 -18
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/sdk/constants.d.ts +2 -0
- package/dist/plugin/sdk/constants.d.ts.map +1 -1
- package/dist/plugin/sdk/constants.js +1 -0
- package/dist/plugin/sdk/constants.js.map +1 -1
- package/dist/plugin/sdk/types.d.ts +18 -4
- package/dist/plugin/sdk/types.d.ts.map +1 -1
- package/dist/plugin/sdk/types.js +1 -1
- package/dist/plugin/sdk/types.js.map +1 -1
- package/dist/plugin/validate.d.ts.map +1 -1
- package/dist/plugin/validate.js +17 -8
- package/dist/plugin/validate.js.map +1 -1
- package/dist/types/severity.d.ts +3 -0
- package/dist/types/severity.d.ts.map +1 -0
- package/dist/types/severity.js +2 -0
- package/dist/types/severity.js.map +1 -0
- package/dist/utils/atomicWrite.d.ts +23 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +44 -0
- package/dist/utils/atomicWrite.js.map +1 -0
- package/dist/utils/concurrency.d.ts +17 -0
- package/dist/utils/concurrency.d.ts.map +1 -0
- package/dist/utils/concurrency.js +38 -0
- package/dist/utils/concurrency.js.map +1 -0
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +44 -33
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/encryption.d.ts.map +1 -1
- package/dist/utils/encryption.js +7 -2
- package/dist/utils/encryption.js.map +1 -1
- package/dist/utils/exitCode.d.ts +2 -0
- package/dist/utils/exitCode.d.ts.map +1 -0
- package/dist/utils/exitCode.js +4 -0
- package/dist/utils/exitCode.js.map +1 -0
- package/dist/utils/fileLock.d.ts.map +1 -1
- package/dist/utils/fileLock.js +177 -30
- package/dist/utils/fileLock.js.map +1 -1
- package/dist/utils/fsMtime.d.ts +32 -0
- package/dist/utils/fsMtime.d.ts.map +1 -0
- package/dist/utils/fsMtime.js +61 -0
- package/dist/utils/fsMtime.js.map +1 -0
- package/dist/utils/fsRetry.d.ts +20 -0
- package/dist/utils/fsRetry.d.ts.map +1 -0
- package/dist/utils/fsRetry.js +56 -0
- package/dist/utils/fsRetry.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/openBrowser.d.ts.map +1 -1
- package/dist/utils/openBrowser.js +3 -2
- package/dist/utils/openBrowser.js.map +1 -1
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +2 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/secureWrite.d.ts +1 -0
- package/dist/utils/secureWrite.d.ts.map +1 -1
- package/dist/utils/secureWrite.js +8 -2
- package/dist/utils/secureWrite.js.map +1 -1
- package/dist/utils/securityLogger.d.ts.map +1 -1
- package/dist/utils/securityLogger.js +16 -4
- package/dist/utils/securityLogger.js.map +1 -1
- package/dist/utils/ssh.d.ts +2 -1
- package/dist/utils/ssh.d.ts.map +1 -1
- package/dist/utils/ssh.js +14 -4
- package/dist/utils/ssh.js.map +1 -1
- package/dist/utils/version.d.ts +5 -0
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +26 -0
- package/dist/utils/version.js.map +1 -1
- package/dist/utils/webhookSecurity.d.ts +13 -0
- package/dist/utils/webhookSecurity.d.ts.map +1 -0
- package/dist/utils/webhookSecurity.js +130 -0
- package/dist/utils/webhookSecurity.js.map +1 -0
- package/kastell-plugin/.claude-plugin/plugin.json +2 -2
- package/kastell-plugin/README.md +6 -0
- package/package.json +3 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kastell",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Server security auditing, hardening, and fleet management.
|
|
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": [
|
|
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.
|
|
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 |
|
|
98
|
+
| Security Checks | 470+ | 300+ | Varies by profile |
|
|
99
99
|
| Auto-Fix | Safe tier | Suggest only | Suggest only |
|
|
100
|
-
| MCP (AI Agent) |
|
|
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 (
|
|
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 -- **
|
|
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 **
|
|
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.
|
|
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 |
|
|
98
|
+
| Guvenlik Kontrolleri | 470+ | 300+ | Profile gore degisir |
|
|
99
99
|
| Otomatik Duzeltme | Guvenli katman | Sadece oneri | Sadece oneri |
|
|
100
|
-
| MCP (AI Ajan) |
|
|
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 (
|
|
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 --
|
|
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,
|
|
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
|
|
package/dist/commands/audit.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
29
|
-
*
|
|
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;
|
|
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"}
|
package/dist/commands/audit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
return;
|
|
172
|
+
throw new AuditError(`Snapshot not found: ${beforeRef}`);
|
|
142
173
|
}
|
|
143
174
|
if (!afterSnap) {
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
return;
|
|
195
|
+
throw new AuditError(`Server not found: ${serverARef}`);
|
|
168
196
|
}
|
|
169
197
|
if (!serverB) {
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
250
|
+
logDiagnostic("warning", "Score methodology updated. New baseline established.");
|
|
226
251
|
}
|
|
227
252
|
else if (trend !== "first audit") {
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
return;
|
|
396
|
+
throw new AuditError("--threshold must be a number");
|
|
354
397
|
}
|
|
355
398
|
if (auditResult.overallScore < threshold) {
|
|
356
|
-
|
|
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 && !
|
|
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
|
-
|
|
380
|
-
return;
|
|
422
|
+
throw new AuditError("--threshold must be a number");
|
|
381
423
|
}
|
|
382
424
|
if (auditResult.overallScore < threshold) {
|
|
383
|
-
|
|
384
|
-
process.exitCode = 1;
|
|
425
|
+
markCommandFailed();
|
|
385
426
|
return;
|
|
386
427
|
}
|
|
387
428
|
}
|