defense-mcp-server 0.6.0 → 0.7.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 (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/build/core/rate-limiter.d.ts +34 -0
  3. package/build/core/rate-limiter.d.ts.map +1 -1
  4. package/build/core/rate-limiter.js +50 -0
  5. package/build/core/sudo-guard.d.ts +17 -0
  6. package/build/core/sudo-guard.d.ts.map +1 -1
  7. package/build/core/sudo-guard.js +45 -1
  8. package/build/core/sudo-session.d.ts +49 -6
  9. package/build/core/sudo-session.d.ts.map +1 -1
  10. package/build/core/sudo-session.js +205 -23
  11. package/build/core/tool-dependencies.d.ts +6 -7
  12. package/build/core/tool-dependencies.d.ts.map +1 -1
  13. package/build/core/tool-dependencies.js +79 -393
  14. package/build/core/tool-registry.d.ts +1 -0
  15. package/build/core/tool-registry.d.ts.map +1 -1
  16. package/build/core/tool-registry.js +111 -340
  17. package/build/core/tool-wrapper.d.ts.map +1 -1
  18. package/build/core/tool-wrapper.js +1 -6
  19. package/build/index.js +22 -3
  20. package/build/tools/access-control.d.ts +4 -2
  21. package/build/tools/access-control.d.ts.map +1 -1
  22. package/build/tools/access-control.js +830 -832
  23. package/build/tools/compliance.d.ts +4 -3
  24. package/build/tools/compliance.d.ts.map +1 -1
  25. package/build/tools/compliance.js +713 -681
  26. package/build/tools/container-security.d.ts +6 -7
  27. package/build/tools/container-security.d.ts.map +1 -1
  28. package/build/tools/container-security.js +283 -243
  29. package/build/tools/drift-detection.d.ts +1 -7
  30. package/build/tools/drift-detection.d.ts.map +1 -1
  31. package/build/tools/drift-detection.js +2 -326
  32. package/build/tools/ebpf-security.d.ts +1 -1
  33. package/build/tools/ebpf-security.d.ts.map +1 -1
  34. package/build/tools/ebpf-security.js +78 -81
  35. package/build/tools/encryption.d.ts +5 -2
  36. package/build/tools/encryption.d.ts.map +1 -1
  37. package/build/tools/encryption.js +294 -378
  38. package/build/tools/firewall.d.ts +2 -2
  39. package/build/tools/firewall.d.ts.map +1 -1
  40. package/build/tools/firewall.js +311 -339
  41. package/build/tools/hardening.d.ts +1 -3
  42. package/build/tools/hardening.d.ts.map +1 -1
  43. package/build/tools/hardening.js +1293 -1381
  44. package/build/tools/ids.d.ts +1 -8
  45. package/build/tools/ids.d.ts.map +1 -1
  46. package/build/tools/ids.js +2 -624
  47. package/build/tools/incident-response.d.ts +4 -3
  48. package/build/tools/incident-response.d.ts.map +1 -1
  49. package/build/tools/incident-response.js +58 -89
  50. package/build/tools/integrity.d.ts +15 -0
  51. package/build/tools/integrity.d.ts.map +1 -0
  52. package/build/tools/integrity.js +1145 -0
  53. package/build/tools/logging.d.ts +15 -6
  54. package/build/tools/logging.d.ts.map +1 -1
  55. package/build/tools/logging.js +915 -144
  56. package/build/tools/malware.d.ts +3 -3
  57. package/build/tools/malware.d.ts.map +1 -1
  58. package/build/tools/malware.js +197 -173
  59. package/build/tools/meta.d.ts +5 -4
  60. package/build/tools/meta.d.ts.map +1 -1
  61. package/build/tools/meta.js +692 -1184
  62. package/build/tools/network-defense.d.ts +4 -5
  63. package/build/tools/network-defense.d.ts.map +1 -1
  64. package/build/tools/network-defense.js +85 -107
  65. package/build/tools/patch-management.d.ts.map +1 -1
  66. package/build/tools/patch-management.js +517 -531
  67. package/build/tools/reporting.d.ts +1 -10
  68. package/build/tools/reporting.d.ts.map +1 -1
  69. package/build/tools/reporting.js +2 -559
  70. package/build/tools/secrets.d.ts +1 -2
  71. package/build/tools/secrets.d.ts.map +1 -1
  72. package/build/tools/secrets.js +479 -515
  73. package/build/tools/siem-integration.d.ts +1 -17
  74. package/build/tools/siem-integration.d.ts.map +1 -1
  75. package/build/tools/siem-integration.js +2 -754
  76. package/build/tools/sudo-management.d.ts +2 -3
  77. package/build/tools/sudo-management.d.ts.map +1 -1
  78. package/build/tools/sudo-management.js +554 -437
  79. package/docs/TOOLS-REFERENCE.md +352 -315
  80. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.7.0] — 2026-03-12
10
+
11
+ ### v0.7.0 — Tool Consolidation & Sudo Hardening Overhaul
12
+
13
+ #### Tool Consolidation (94 → 31 tools, −67%, zero capability loss)
14
+ - Merged all 94 granular MCP tools into 31 domain-grouped tools using action discriminators
15
+ - Every previous capability preserved via `action` parameters within each consolidated tool
16
+ - Reduces MCP registration overhead by 67% while maintaining full security coverage
17
+ - All 1,802 tests passing across 62 test files
18
+
19
+ #### Sudo Hardening
20
+ - **Removed `NOPASSWD: ALL`** sudoers grant — eliminated overly-broad privilege escalation
21
+ - **Scoped allowlist** (`etc/sudoers.d/mcpuser`): 94-command explicit allowlist covering only required security binaries (iptables, ufw, aide, rkhunter, clamav, auditd, etc.)
22
+ - **NOPASSWD regression detection**: `SudoGuard.checkNopasswdConfiguration()` runs at server startup to detect any re-introduction of broad sudo grants
23
+ - **Rate limiting** on sudo elevation: `RateLimiter` wired into `SudoSession.elevate()` (5 attempts per 5-minute window)
24
+ - **Structured audit trail**: `logger.security()` emits JSON audit events for all elevation, drop, extension, and timeout events
25
+
26
+ #### Docker Entrypoint
27
+ - New `docker-entrypoint.sh`: sets `mcpuser` password from Docker secret (`/run/secrets/mcpuser_password`) or `MCPUSER_PASSWORD` env var at container startup
28
+ - Credentials zeroed from environment after use, privileges dropped before handing off to the MCP server process
29
+ - Prevents hardcoded or empty passwords in container images
30
+
31
+ #### New Modules
32
+ - `src/tools/integrity.ts` — Absorbs and supersedes `ids.ts` + `drift-detection.ts`; unified integrity checking with IDS baseline management, drift detection, and file integrity verification
33
+ - `etc/sudoers.d/mcpuser` — Scoped sudoers allowlist (94 commands, no wildcards)
34
+ - `docker-entrypoint.sh` — Secure container password bootstrap script
35
+
36
+ #### Infrastructure
37
+ - `src/core/sudo-session.ts` — Integrated `RateLimiter` for elevation throttling
38
+ - `src/core/sudo-guard.ts` — Added `checkNopasswdConfiguration()` startup regression check
39
+ - `src/core/rate-limiter.ts` — Extended to support sudo-specific rate limiting context
40
+ - Updated `TOOLS-REFERENCE.md` and `TOOL-CONSOLIDATION-PLAN.md` to reflect 31-tool architecture
41
+
42
+ ---
43
+
9
44
  ## [0.6.0] — 2026-03-09
10
45
 
11
46
  ### v0.6.0 — 16 New Security Tools
@@ -48,6 +48,40 @@ export declare class RateLimiter {
48
48
  static instance(): RateLimiter;
49
49
  /** Reset the singleton (for testing). */
50
50
  static resetInstance(): void;
51
+ /**
52
+ * Inspect current rate limit state for `key` WITHOUT recording an invocation.
53
+ *
54
+ * Use this to check whether a call would be allowed before deciding to
55
+ * proceed. Unlike `check()`, this does NOT consume an attempt slot.
56
+ *
57
+ * @param key - Identifier to inspect (e.g., a tool name or auth key)
58
+ * @returns Current limit state: allowed, remaining attempts, and optional
59
+ * retryAfterMs (ms until the oldest timestamp expires and a slot opens)
60
+ */
61
+ peek(key: string): {
62
+ allowed: boolean;
63
+ remaining: number;
64
+ retryAfterMs?: number;
65
+ };
66
+ /**
67
+ * Record a failure for `key` without performing a limit check.
68
+ *
69
+ * Use this to register a failed attempt (e.g., wrong password) after the
70
+ * fact. Unlike `check()`, this does NOT gate on the current limit state —
71
+ * it unconditionally adds a timestamp to the bucket.
72
+ *
73
+ * @param key - Identifier to record against
74
+ */
75
+ record(key: string): void;
76
+ /**
77
+ * Reset the rate limit counter for a specific `key`.
78
+ *
79
+ * Use after a confirmed successful authentication to clear the failed-attempt
80
+ * counter so a subsequent failure starts from zero.
81
+ *
82
+ * @param key - Identifier whose bucket should be cleared
83
+ */
84
+ resetKey(key: string): void;
51
85
  /**
52
86
  * Check whether an invocation of `toolName` is allowed, and if so,
53
87
  * record it. Returns a {@link RateLimitResult} indicating whether the
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/core/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;CACzB;AAID;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACtB,kCAAkC;IAClC,OAAO,CAAC,WAAW,CAAkC;IACrD,+BAA+B;IAC/B,OAAO,CAAC,YAAY,CAA8B;IAElD,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;gBAExC,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAMtE,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,IAAI,WAAW;IAO9B,yCAAyC;IACzC,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;;;;;OAOG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe;IA8CxC,gDAAgD;IAChD,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,WAAW;CAMpB"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/core/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;CACzB;AAID;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACtB,kCAAkC;IAClC,OAAO,CAAC,WAAW,CAAkC;IACrD,+BAA+B;IAC/B,OAAO,CAAC,YAAY,CAA8B;IAElD,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;gBAExC,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAMtE,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,IAAI,WAAW;IAO9B,yCAAyC;IACzC,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;;;;;;;OASG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE;IAkBjF;;;;;;;;OAQG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMzB;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI3B;;;;;;;OAOG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe;IA8CxC,gDAAgD;IAChD,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,WAAW;CAMpB"}
@@ -49,6 +49,56 @@ export class RateLimiter {
49
49
  static resetInstance() {
50
50
  RateLimiter._instance = null;
51
51
  }
52
+ /**
53
+ * Inspect current rate limit state for `key` WITHOUT recording an invocation.
54
+ *
55
+ * Use this to check whether a call would be allowed before deciding to
56
+ * proceed. Unlike `check()`, this does NOT consume an attempt slot.
57
+ *
58
+ * @param key - Identifier to inspect (e.g., a tool name or auth key)
59
+ * @returns Current limit state: allowed, remaining attempts, and optional
60
+ * retryAfterMs (ms until the oldest timestamp expires and a slot opens)
61
+ */
62
+ peek(key) {
63
+ const now = Date.now();
64
+ this.pruneTool(key, now);
65
+ const bucket = this.getToolBucket(key);
66
+ const count = bucket.timestamps.length;
67
+ if (this.maxPerTool > 0 && count >= this.maxPerTool) {
68
+ const oldest = bucket.timestamps[0] ?? now;
69
+ const retryAfterMs = Math.max(0, oldest + this.windowMs - now);
70
+ return { allowed: false, remaining: 0, retryAfterMs };
71
+ }
72
+ return {
73
+ allowed: true,
74
+ remaining: this.maxPerTool > 0 ? this.maxPerTool - count : Infinity,
75
+ };
76
+ }
77
+ /**
78
+ * Record a failure for `key` without performing a limit check.
79
+ *
80
+ * Use this to register a failed attempt (e.g., wrong password) after the
81
+ * fact. Unlike `check()`, this does NOT gate on the current limit state —
82
+ * it unconditionally adds a timestamp to the bucket.
83
+ *
84
+ * @param key - Identifier to record against
85
+ */
86
+ record(key) {
87
+ const now = Date.now();
88
+ this.pruneTool(key, now);
89
+ this.getToolBucket(key).timestamps.push(now);
90
+ }
91
+ /**
92
+ * Reset the rate limit counter for a specific `key`.
93
+ *
94
+ * Use after a confirmed successful authentication to clear the failed-attempt
95
+ * counter so a subsequent failure starts from zero.
96
+ *
97
+ * @param key - Identifier whose bucket should be cleared
98
+ */
99
+ resetKey(key) {
100
+ this.toolBuckets.delete(key);
101
+ }
52
102
  /**
53
103
  * Check whether an invocation of `toolName` is allowed, and if so,
54
104
  * record it. Returns a {@link RateLimitResult} indicating whether the
@@ -65,6 +65,23 @@ export interface ElevationPromptResponse {
65
65
  * generation. All methods are stateless and can be called directly.
66
66
  */
67
67
  export declare class SudoGuard {
68
+ /**
69
+ * Check whether passwordless sudo (`NOPASSWD: ALL`) is still active.
70
+ *
71
+ * Reads `/etc/sudoers` and `/etc/sudoers.d/mcpuser` (non-root readable
72
+ * paths only) and searches for the `NOPASSWD:.*ALL` pattern. If found,
73
+ * logs a CRITICAL security warning — the credential validation in
74
+ * `SudoSession.elevate()` is hollow while this grant exists.
75
+ *
76
+ * This check is intended to be called during server startup so operators
77
+ * are alerted immediately if the Docker image was not rebuilt correctly.
78
+ *
79
+ * @returns `{ nopasswdDetected: boolean, location?: string }`
80
+ */
81
+ static checkNopasswdConfiguration(): {
82
+ nopasswdDetected: boolean;
83
+ location?: string;
84
+ };
68
85
  /**
69
86
  * Check whether command output (stderr and/or stdout) indicates a
70
87
  * permission/privilege failure.
@@ -1 +1 @@
1
- {"version":3,"file":"sudo-guard.d.ts","sourceRoot":"","sources":["../../src/core/sudo-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAqFH;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE;QACL,qDAAqD;QACrD,iBAAiB,EAAE,IAAI,CAAC;QACxB;;;;WAIG;QACH,YAAY,EAAE,IAAI,CAAC;QACnB,2BAA2B;QAC3B,UAAU,EAAE,MAAM,CAAC;QACnB,8BAA8B;QAC9B,MAAM,EAAE,MAAM,CAAC;QACf,qCAAqC;QACrC,aAAa,EAAE,cAAc,CAAC;KAC/B,CAAC;CACH;AAID;;;GAGG;AACH,qBAAa,SAAS;IACpB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAqBpE;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,qBAAqB,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,uBAAuB;IAqE1B;;;;;;;;;OASG;IACH,MAAM,CAAC,yBAAyB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5C,OAAO;IA4BV;;;OAGG;IACH,MAAM,CAAC,mBAAmB,CACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5C,MAAM,GAAG,SAAS;IAoBrB;;;OAGG;IACH,MAAM,CAAC,gBAAgB,IAAI,OAAO;IAIlC;;;;;;;;;OASG;IACH,MAAM,CAAC,eAAe,IAAI;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAU7D;;;;;;;;;;OAUG;IACH,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAiDrF"}
1
+ {"version":3,"file":"sudo-guard.d.ts","sourceRoot":"","sources":["../../src/core/sudo-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAsFH;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE;QACL,qDAAqD;QACrD,iBAAiB,EAAE,IAAI,CAAC;QACxB;;;;WAIG;QACH,YAAY,EAAE,IAAI,CAAC;QACnB,2BAA2B;QAC3B,UAAU,EAAE,MAAM,CAAC;QACnB,8BAA8B;QAC9B,MAAM,EAAE,MAAM,CAAC;QACf,qCAAqC;QACrC,aAAa,EAAE,cAAc,CAAC;KAC/B,CAAC;CACH;AAID;;;GAGG;AACH,qBAAa,SAAS;IACpB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,0BAA0B,IAAI;QAAE,gBAAgB,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IA2CrF;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAqBpE;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,qBAAqB,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,uBAAuB;IAqE1B;;;;;;;;;OASG;IACH,MAAM,CAAC,yBAAyB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5C,OAAO;IA4BV;;;OAGG;IACH,MAAM,CAAC,mBAAmB,CACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5C,MAAM,GAAG,SAAS;IAoBrB;;;OAGG;IACH,MAAM,CAAC,gBAAgB,IAAI,OAAO;IAIlC;;;;;;;;;OASG;IACH,MAAM,CAAC,eAAe,IAAI;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAU7D;;;;;;;;;;OAUG;IACH,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAiDrF"}
@@ -34,7 +34,8 @@
34
34
  * @module sudo-guard
35
35
  */
36
36
  import { SudoSession } from "./sudo-session.js";
37
- import { lstatSync } from "node:fs";
37
+ import { lstatSync, readFileSync } from "node:fs";
38
+ import { logger } from "./logger.js";
38
39
  // ── Permission Error Detection ───────────────────────────────────────────────
39
40
  /**
40
41
  * Patterns in stderr/stdout that indicate a permission/privilege failure.
@@ -108,6 +109,49 @@ const PERMISSION_EXIT_CODES = new Set([
108
109
  * generation. All methods are stateless and can be called directly.
109
110
  */
110
111
  export class SudoGuard {
112
+ /**
113
+ * Check whether passwordless sudo (`NOPASSWD: ALL`) is still active.
114
+ *
115
+ * Reads `/etc/sudoers` and `/etc/sudoers.d/mcpuser` (non-root readable
116
+ * paths only) and searches for the `NOPASSWD:.*ALL` pattern. If found,
117
+ * logs a CRITICAL security warning — the credential validation in
118
+ * `SudoSession.elevate()` is hollow while this grant exists.
119
+ *
120
+ * This check is intended to be called during server startup so operators
121
+ * are alerted immediately if the Docker image was not rebuilt correctly.
122
+ *
123
+ * @returns `{ nopasswdDetected: boolean, location?: string }`
124
+ */
125
+ static checkNopasswdConfiguration() {
126
+ const candidatePaths = [
127
+ "/etc/sudoers",
128
+ "/etc/sudoers.d/mcpuser",
129
+ "/etc/sudoers.d/",
130
+ ];
131
+ const nopasswdPattern = /NOPASSWD\s*:\s*ALL/i;
132
+ for (const filePath of candidatePaths) {
133
+ try {
134
+ const content = readFileSync(filePath, "utf-8");
135
+ if (nopasswdPattern.test(content)) {
136
+ logger.security("sudo-guard", "nopasswd_detected", "SECURITY CRITICAL: NOPASSWD:ALL detected in sudoers configuration. " +
137
+ "Authentication via sudo_elevate is NON-FUNCTIONAL — any password will be accepted. " +
138
+ "Remove NOPASSWD from the sudoers file and set a real password for mcpuser. " +
139
+ "See docs/SUDO-SESSION-DESIGN.md for remediation steps.", {
140
+ severity: "CRITICAL",
141
+ location: filePath,
142
+ remediation: "Rebuild the Docker image with the updated Dockerfile that uses " +
143
+ "etc/sudoers.d/mcpuser (scoped allowlist, no NOPASSWD).",
144
+ });
145
+ return { nopasswdDetected: true, location: filePath };
146
+ }
147
+ }
148
+ catch {
149
+ // File may not exist or may not be readable — that's fine
150
+ }
151
+ }
152
+ logger.info("sudo-guard", "nopasswd_check_passed", "Sudoers NOPASSWD:ALL check passed — passwordless sudo not detected");
153
+ return { nopasswdDetected: false };
154
+ }
111
155
  /**
112
156
  * Check whether command output (stderr and/or stdout) indicates a
113
157
  * permission/privilege failure.
@@ -12,8 +12,11 @@
12
12
  * - Auto-expires after a configurable timeout (default 15 minutes)
13
13
  * - Explicit `drop()` zeroes the buffer immediately
14
14
  * - Process exit handler zeroes the buffer on shutdown
15
- * - Validates credentials before storing (test with `sudo -S -v`)
15
+ * - Validates credentials before storing (test with `sudo -S -k -v`)
16
16
  * - Never logs or exposes the password in any output
17
+ * - Rate limits failed authentication attempts (5 per 5 minutes)
18
+ * - Session UID guard: drops session if OS UID changes mid-session
19
+ * - Emits structured audit events for all state transitions
17
20
  *
18
21
  * Child process spawning goes through spawn-safe.ts which enforces the
19
22
  * command allowlist and shell: false without creating circular dependencies.
@@ -23,6 +26,21 @@ export interface SudoSessionStatus {
23
26
  username: string | null;
24
27
  expiresAt: string | null;
25
28
  remainingSeconds: number | null;
29
+ /** Rate-limit info (safe to surface to MCP callers). */
30
+ rateLimit: {
31
+ limited: boolean;
32
+ attemptsRemaining: number;
33
+ resetAt?: string;
34
+ };
35
+ }
36
+ /** Structured result from {@link SudoSession.elevate}. */
37
+ export interface ElevateResult {
38
+ success: boolean;
39
+ error?: string;
40
+ /** Present when the elevation was blocked by rate limiting. */
41
+ rateLimited?: boolean;
42
+ /** Present when rate-limited: ms until the lockout window resets. */
43
+ retryAfterMs?: number;
26
44
  }
27
45
  export declare class SudoSession {
28
46
  /** Password stored in a Buffer so we can zero it (not interned by V8). */
@@ -41,10 +59,18 @@ export declare class SudoSession {
41
59
  private sessionUserId;
42
60
  /** Timestamp (epoch ms) when the session expires. */
43
61
  private expiresAt;
62
+ /** Epoch ms when the current session was established (for drop duration calc). */
63
+ private elevatedAt;
44
64
  /** Handle for the auto-expiry timer. */
45
65
  private expiryTimer;
46
66
  /** Default session timeout in milliseconds (15 min). */
47
67
  private defaultTimeoutMs;
68
+ /**
69
+ * Per-session rate limiter for authentication attempts.
70
+ * Configured with a sliding 5-minute window and 5-attempt cap.
71
+ * Uses per-key tracking via `peek()` / `record()` / `resetKey()`.
72
+ */
73
+ private authRateLimiter;
48
74
  private constructor();
49
75
  /** Get the singleton instance. */
50
76
  static getInstance(): SudoSession;
@@ -67,16 +93,22 @@ export declare class SudoSession {
67
93
  * `-S` reads password from stdin.
68
94
  * `-p ""` suppresses the password prompt text.
69
95
  *
70
- * @returns result indicating success or failure with error message.
96
+ * Phase 2: Checks the auth rate limiter before attempting. Records failures.
97
+ * Resets the counter on success.
98
+ *
99
+ * Phase 3: Emits structured audit events for all outcomes.
100
+ *
101
+ * @returns Structured result indicating success, failure, or rate-limit block.
71
102
  */
72
- elevate(password: string | Buffer, timeoutMs?: number): Promise<{
73
- success: boolean;
74
- error?: string;
75
- }>;
103
+ elevate(password: string | Buffer, timeoutMs?: number): Promise<ElevateResult>;
76
104
  /**
77
105
  * Returns a **copy** of the password Buffer for piping to sudo -S,
78
106
  * or null if not elevated.
79
107
  *
108
+ * SECURITY (CICD-028): Validates that the calling process UID matches the
109
+ * UID that established the session. If the UID has changed (e.g., due to
110
+ * a UID-switching attack), the session is dropped and null is returned.
111
+ *
80
112
  * The caller MUST zero the returned Buffer with `.fill(0)` after use.
81
113
  * A copy is returned so the original can be zeroed independently via `drop()`.
82
114
  */
@@ -85,6 +117,15 @@ export declare class SudoSession {
85
117
  isElevated(): boolean;
86
118
  /** Get current session status (safe to expose via MCP). */
87
119
  getStatus(): SudoSessionStatus;
120
+ /**
121
+ * Get the current rate-limit status for the auth key.
122
+ * Safe to surface to MCP callers (contains no credentials).
123
+ */
124
+ getRateLimitStatus(): {
125
+ limited: boolean;
126
+ attemptsRemaining: number;
127
+ resetAt?: string;
128
+ };
88
129
  /**
89
130
  * Drop elevated privileges immediately.
90
131
  * Zeroes the password buffer and clears all session state.
@@ -94,6 +135,8 @@ export declare class SudoSession {
94
135
  * Extend the session timeout by the given milliseconds (or the default).
95
136
  */
96
137
  extend(extraMs?: number): boolean;
138
+ /** Called when the TTL timer fires. Emits audit event then drops. */
139
+ private _onSessionExpired;
97
140
  private storePassword;
98
141
  private isExpired;
99
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sudo-session.d.ts","sourceRoot":"","sources":["../../src/core/sudo-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAsED,qBAAa,WAAW;IACtB,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAuB;IAE1C,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAuB;IAEvC;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa,CAAuB;IAE5C,qDAAqD;IACrD,OAAO,CAAC,SAAS,CAAuB;IAExC,wCAAwC;IACxC,OAAO,CAAC,WAAW,CAA8C;IAEjE,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAkB;IAE1C,OAAO;IASP,kCAAkC;IAClC,MAAM,CAAC,WAAW,IAAI,WAAW;IAOjC;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMnC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAuE3G;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI;IAc5B,wDAAwD;IACxD,UAAU,IAAI,OAAO;IAUrB,2DAA2D;IAC3D,SAAS,IAAI,iBAAiB;IAsB9B;;;OAGG;IACH,IAAI,IAAI,IAAI;IAwBZ;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAyBjC,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,SAAS;CAIlB"}
1
+ {"version":3,"file":"sudo-session.d.ts","sourceRoot":"","sources":["../../src/core/sudo-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wDAAwD;IACxD,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAuFD,qBAAa,WAAW;IACtB,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAuB;IAE1C,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAuB;IAEvC;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa,CAAuB;IAE5C,qDAAqD;IACrD,OAAO,CAAC,SAAS,CAAuB;IAExC,kFAAkF;IAClF,OAAO,CAAC,UAAU,CAAuB;IAEzC,wCAAwC;IACxC,OAAO,CAAC,WAAW,CAA8C;IAEjE,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAkB;IAE1C;;;;OAIG;IACH,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO;IAiBP,kCAAkC;IAClC,MAAM,CAAC,WAAW,IAAI,WAAW;IAOjC;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMnC;;;;;;;;;;;;;;;OAeG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA+JpF;;;;;;;;;;OAUG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI;IA2B5B,wDAAwD;IACxD,UAAU,IAAI,OAAO;IAUrB,2DAA2D;IAC3D,SAAS,IAAI,iBAAiB;IAmC9B;;;OAGG;IACH,kBAAkB,IAAI;QACpB,OAAO,EAAE,OAAO,CAAC;QACjB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB;IAWD;;;OAGG;IACH,IAAI,IAAI,IAAI;IAuCZ;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAiCjC,qEAAqE;IACrE,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,SAAS;CAIlB"}