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.
- package/CHANGELOG.md +35 -0
- package/build/core/rate-limiter.d.ts +34 -0
- package/build/core/rate-limiter.d.ts.map +1 -1
- package/build/core/rate-limiter.js +50 -0
- package/build/core/sudo-guard.d.ts +17 -0
- package/build/core/sudo-guard.d.ts.map +1 -1
- package/build/core/sudo-guard.js +45 -1
- package/build/core/sudo-session.d.ts +49 -6
- package/build/core/sudo-session.d.ts.map +1 -1
- package/build/core/sudo-session.js +205 -23
- package/build/core/tool-dependencies.d.ts +6 -7
- package/build/core/tool-dependencies.d.ts.map +1 -1
- package/build/core/tool-dependencies.js +79 -393
- package/build/core/tool-registry.d.ts +1 -0
- package/build/core/tool-registry.d.ts.map +1 -1
- package/build/core/tool-registry.js +111 -340
- package/build/core/tool-wrapper.d.ts.map +1 -1
- package/build/core/tool-wrapper.js +1 -6
- package/build/index.js +22 -3
- package/build/tools/access-control.d.ts +4 -2
- package/build/tools/access-control.d.ts.map +1 -1
- package/build/tools/access-control.js +830 -832
- package/build/tools/compliance.d.ts +4 -3
- package/build/tools/compliance.d.ts.map +1 -1
- package/build/tools/compliance.js +713 -681
- package/build/tools/container-security.d.ts +6 -7
- package/build/tools/container-security.d.ts.map +1 -1
- package/build/tools/container-security.js +283 -243
- package/build/tools/drift-detection.d.ts +1 -7
- package/build/tools/drift-detection.d.ts.map +1 -1
- package/build/tools/drift-detection.js +2 -326
- package/build/tools/ebpf-security.d.ts +1 -1
- package/build/tools/ebpf-security.d.ts.map +1 -1
- package/build/tools/ebpf-security.js +78 -81
- package/build/tools/encryption.d.ts +5 -2
- package/build/tools/encryption.d.ts.map +1 -1
- package/build/tools/encryption.js +294 -378
- package/build/tools/firewall.d.ts +2 -2
- package/build/tools/firewall.d.ts.map +1 -1
- package/build/tools/firewall.js +311 -339
- package/build/tools/hardening.d.ts +1 -3
- package/build/tools/hardening.d.ts.map +1 -1
- package/build/tools/hardening.js +1293 -1381
- package/build/tools/ids.d.ts +1 -8
- package/build/tools/ids.d.ts.map +1 -1
- package/build/tools/ids.js +2 -624
- package/build/tools/incident-response.d.ts +4 -3
- package/build/tools/incident-response.d.ts.map +1 -1
- package/build/tools/incident-response.js +58 -89
- package/build/tools/integrity.d.ts +15 -0
- package/build/tools/integrity.d.ts.map +1 -0
- package/build/tools/integrity.js +1145 -0
- package/build/tools/logging.d.ts +15 -6
- package/build/tools/logging.d.ts.map +1 -1
- package/build/tools/logging.js +915 -144
- package/build/tools/malware.d.ts +3 -3
- package/build/tools/malware.d.ts.map +1 -1
- package/build/tools/malware.js +197 -173
- package/build/tools/meta.d.ts +5 -4
- package/build/tools/meta.d.ts.map +1 -1
- package/build/tools/meta.js +692 -1184
- package/build/tools/network-defense.d.ts +4 -5
- package/build/tools/network-defense.d.ts.map +1 -1
- package/build/tools/network-defense.js +85 -107
- package/build/tools/patch-management.d.ts.map +1 -1
- package/build/tools/patch-management.js +517 -531
- package/build/tools/reporting.d.ts +1 -10
- package/build/tools/reporting.d.ts.map +1 -1
- package/build/tools/reporting.js +2 -559
- package/build/tools/secrets.d.ts +1 -2
- package/build/tools/secrets.d.ts.map +1 -1
- package/build/tools/secrets.js +479 -515
- package/build/tools/siem-integration.d.ts +1 -17
- package/build/tools/siem-integration.d.ts.map +1 -1
- package/build/tools/siem-integration.js +2 -754
- package/build/tools/sudo-management.d.ts +2 -3
- package/build/tools/sudo-management.d.ts.map +1 -1
- package/build/tools/sudo-management.js +554 -437
- package/docs/TOOLS-REFERENCE.md +352 -315
- 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;
|
|
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"}
|
package/build/core/sudo-guard.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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"}
|