defense-mcp-server 0.6.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 +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- package/package.json +87 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrivilegeManager — detects the current privilege level and checks whether
|
|
3
|
+
* a tool's privilege requirements are satisfied.
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the pre-flight validation system. It queries:
|
|
6
|
+
* - UID / EUID via `process.getuid()` / `process.geteuid()`
|
|
7
|
+
* - Linux capabilities via `/proc/self/status` CapEff bitmask
|
|
8
|
+
* - Passwordless sudo via `sudo -n true`
|
|
9
|
+
* - Active sudo session via `SudoSession.getInstance().isElevated()`
|
|
10
|
+
* - User groups via `id -Gn`
|
|
11
|
+
*
|
|
12
|
+
* Child process spawning goes through spawn-safe.ts which enforces the
|
|
13
|
+
* command allowlist and shell: false without creating circular dependencies.
|
|
14
|
+
*
|
|
15
|
+
* @module privilege-manager
|
|
16
|
+
*/
|
|
17
|
+
import type { ToolManifest } from "./tool-registry.js";
|
|
18
|
+
export interface PrivilegeStatus {
|
|
19
|
+
/** Current real user ID */
|
|
20
|
+
uid: number;
|
|
21
|
+
/** Current effective user ID */
|
|
22
|
+
euid: number;
|
|
23
|
+
/** Whether running as root (euid === 0) */
|
|
24
|
+
isRoot: boolean;
|
|
25
|
+
/** Whether `sudo` binary is available on PATH */
|
|
26
|
+
sudoAvailable: boolean;
|
|
27
|
+
/** Whether passwordless sudo works (`sudo -n true`) */
|
|
28
|
+
passwordlessSudo: boolean;
|
|
29
|
+
/** Whether SudoSession has cached credentials */
|
|
30
|
+
sudoSessionActive: boolean;
|
|
31
|
+
/** Currently held Linux capabilities (from CapEff) */
|
|
32
|
+
capabilities: Set<string>;
|
|
33
|
+
/** User's group memberships */
|
|
34
|
+
groups: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface PrivilegeCheckResult {
|
|
37
|
+
/** All privilege requirements met */
|
|
38
|
+
satisfied: boolean;
|
|
39
|
+
/** Problems found */
|
|
40
|
+
issues: PrivilegeIssue[];
|
|
41
|
+
/** How to fix any issues */
|
|
42
|
+
recommendations: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface PrivilegeIssue {
|
|
45
|
+
type: "sudo-required" | "capability-missing" | "sudo-unavailable" | "session-expired";
|
|
46
|
+
/** Human-readable description of the issue */
|
|
47
|
+
description: string;
|
|
48
|
+
/** Which tool/operation needs this privilege */
|
|
49
|
+
operation: string;
|
|
50
|
+
/** How to resolve the issue */
|
|
51
|
+
resolution: string;
|
|
52
|
+
}
|
|
53
|
+
export declare class PrivilegeManager {
|
|
54
|
+
private cachedStatus;
|
|
55
|
+
private cacheExpiry;
|
|
56
|
+
private static readonly CACHE_TTL;
|
|
57
|
+
private static _instance;
|
|
58
|
+
private constructor();
|
|
59
|
+
/** Get or create the singleton instance. */
|
|
60
|
+
static instance(): PrivilegeManager;
|
|
61
|
+
/**
|
|
62
|
+
* Detect current privilege level.
|
|
63
|
+
* Results are cached for {@link CACHE_TTL} ms to avoid repeated
|
|
64
|
+
* subprocess spawns on rapid sequential tool calls.
|
|
65
|
+
*/
|
|
66
|
+
getStatus(): Promise<PrivilegeStatus>;
|
|
67
|
+
/**
|
|
68
|
+
* Check whether a specific tool's privilege requirements are met.
|
|
69
|
+
*
|
|
70
|
+
* Evaluates the tool's `sudo` level and `capabilities` list against
|
|
71
|
+
* the current {@link PrivilegeStatus} and returns actionable issues.
|
|
72
|
+
*/
|
|
73
|
+
checkForTool(manifest: ToolManifest): Promise<PrivilegeCheckResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Check if a specific Linux capability is in the current effective set.
|
|
76
|
+
*/
|
|
77
|
+
hasCapability(cap: string): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Parse the effective capability set from `/proc/self/status`.
|
|
80
|
+
*
|
|
81
|
+
* Reads the `CapEff` line which contains a hex-encoded bitmask,
|
|
82
|
+
* then maps set bits to capability names using the kernel-defined
|
|
83
|
+
* bit positions.
|
|
84
|
+
*/
|
|
85
|
+
getCurrentCapabilities(): Promise<Set<string>>;
|
|
86
|
+
/**
|
|
87
|
+
* Test whether passwordless sudo works by running `sudo -n true`.
|
|
88
|
+
* The `-n` (non-interactive) flag causes sudo to fail immediately
|
|
89
|
+
* rather than prompting if a password is required.
|
|
90
|
+
*/
|
|
91
|
+
testPasswordlessSudo(): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Check whether the `sudo` binary exists on PATH.
|
|
94
|
+
*/
|
|
95
|
+
isSudoAvailable(): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* Invalidate the cached status.
|
|
98
|
+
* Should be called after events that change privilege state,
|
|
99
|
+
* e.g., after `sudo_elevate` or `sudo_drop`.
|
|
100
|
+
*/
|
|
101
|
+
clearCache(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Get user group memberships via `id -Gn`.
|
|
104
|
+
* Returns an empty array on failure.
|
|
105
|
+
*/
|
|
106
|
+
private getGroups;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=privilege-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privilege-manager.d.ts","sourceRoot":"","sources":["../../src/core/privilege-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAoEvD,MAAM,WAAW,eAAe;IAC9B,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,aAAa,EAAE,OAAO,CAAC;IACvB,uDAAuD;IACvD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iDAAiD;IACjD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sDAAsD;IACtD,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,+BAA+B;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,4BAA4B;IAC5B,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EACA,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB;AA+ED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAU;IAE3C,OAAO,CAAC,MAAM,CAAC,SAAS,CAAiC;IAEzD,OAAO;IAIP,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,IAAI,gBAAgB;IASnC;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC;IAoC3C;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAkGzE;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKlD;;;;;;OAMG;IACG,sBAAsB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAkCpD;;;;OAIG;IACG,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;IAM9C;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAKzC;;;;OAIG;IACH,UAAU,IAAI,IAAI;IAOlB;;;OAGG;IACH,OAAO,CAAC,SAAS;CAUlB"}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrivilegeManager — detects the current privilege level and checks whether
|
|
3
|
+
* a tool's privilege requirements are satisfied.
|
|
4
|
+
*
|
|
5
|
+
* This module is part of the pre-flight validation system. It queries:
|
|
6
|
+
* - UID / EUID via `process.getuid()` / `process.geteuid()`
|
|
7
|
+
* - Linux capabilities via `/proc/self/status` CapEff bitmask
|
|
8
|
+
* - Passwordless sudo via `sudo -n true`
|
|
9
|
+
* - Active sudo session via `SudoSession.getInstance().isElevated()`
|
|
10
|
+
* - User groups via `id -Gn`
|
|
11
|
+
*
|
|
12
|
+
* Child process spawning goes through spawn-safe.ts which enforces the
|
|
13
|
+
* command allowlist and shell: false without creating circular dependencies.
|
|
14
|
+
*
|
|
15
|
+
* @module privilege-manager
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, existsSync, lstatSync } from "node:fs";
|
|
18
|
+
import { execFileSafe } from "./spawn-safe.js";
|
|
19
|
+
import { SudoSession } from "./sudo-session.js";
|
|
20
|
+
// ── Askpass helper detection (mirrors executor.ts) ───────────────────────────
|
|
21
|
+
const ASKPASS_CANDIDATES = [
|
|
22
|
+
"/usr/bin/ssh-askpass",
|
|
23
|
+
"/usr/bin/ksshaskpass",
|
|
24
|
+
"/usr/lib/ssh/x11-ssh-askpass",
|
|
25
|
+
"/usr/libexec/openssh/gnome-ssh-askpass",
|
|
26
|
+
"/usr/bin/lxqt-sudo",
|
|
27
|
+
];
|
|
28
|
+
let cachedAskpassAvailable = null;
|
|
29
|
+
/**
|
|
30
|
+
* Check whether a graphical askpass helper is available on the system.
|
|
31
|
+
* When available, `sudo -A` can pop a secure GUI dialog for the password,
|
|
32
|
+
* so tools should NOT be blocked at pre-flight for missing sudo sessions.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* SECURITY (CORE-016): Validate an askpass candidate's ownership and permissions.
|
|
36
|
+
* This is a local implementation to avoid circular dependency with sudo-guard.ts.
|
|
37
|
+
* Mirrors SudoGuard.validateAskpassPath() logic.
|
|
38
|
+
*/
|
|
39
|
+
function isAskpassCandidateValid(candidatePath) {
|
|
40
|
+
try {
|
|
41
|
+
const lstats = lstatSync(candidatePath);
|
|
42
|
+
if (lstats.isSymbolicLink() || !lstats.isFile())
|
|
43
|
+
return false;
|
|
44
|
+
const currentUid = process.getuid?.() ?? -1;
|
|
45
|
+
if (lstats.uid !== 0 && lstats.uid !== currentUid)
|
|
46
|
+
return false;
|
|
47
|
+
const perms = lstats.mode & 0o777;
|
|
48
|
+
if ((perms & 0o077) !== 0)
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isAskpassAvailable() {
|
|
57
|
+
if (cachedAskpassAvailable !== null)
|
|
58
|
+
return cachedAskpassAvailable;
|
|
59
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
60
|
+
if (!hasDisplay) {
|
|
61
|
+
cachedAskpassAvailable = false;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// SECURITY (CORE-016): Validate env-specified askpass helper
|
|
65
|
+
const envAskpass = process.env.SUDO_ASKPASS;
|
|
66
|
+
if (envAskpass && existsSync(envAskpass) && isAskpassCandidateValid(envAskpass)) {
|
|
67
|
+
cachedAskpassAvailable = true;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// SECURITY (CORE-016): Validate each candidate before accepting
|
|
71
|
+
for (const candidate of ASKPASS_CANDIDATES) {
|
|
72
|
+
if (existsSync(candidate) && isAskpassCandidateValid(candidate)) {
|
|
73
|
+
cachedAskpassAvailable = true;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cachedAskpassAvailable = false;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// ── Linux Capability Bit → Name Mapping ──────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Maps bit position to Linux capability name.
|
|
83
|
+
* Derived from the kernel's `include/uapi/linux/capability.h`.
|
|
84
|
+
*/
|
|
85
|
+
const CAPABILITY_NAMES = [
|
|
86
|
+
"CAP_CHOWN", // 0
|
|
87
|
+
"CAP_DAC_OVERRIDE", // 1
|
|
88
|
+
"CAP_DAC_READ_SEARCH", // 2
|
|
89
|
+
"CAP_FOWNER", // 3
|
|
90
|
+
"CAP_FSETID", // 4
|
|
91
|
+
"CAP_KILL", // 5
|
|
92
|
+
"CAP_SETGID", // 6
|
|
93
|
+
"CAP_SETUID", // 7
|
|
94
|
+
"CAP_SETPCAP", // 8
|
|
95
|
+
"CAP_LINUX_IMMUTABLE", // 9
|
|
96
|
+
"CAP_NET_BIND_SERVICE", // 10
|
|
97
|
+
"CAP_NET_BROADCAST", // 11
|
|
98
|
+
"CAP_NET_ADMIN", // 12
|
|
99
|
+
"CAP_NET_RAW", // 13
|
|
100
|
+
"CAP_IPC_LOCK", // 14
|
|
101
|
+
"CAP_IPC_OWNER", // 15
|
|
102
|
+
"CAP_SYS_MODULE", // 16
|
|
103
|
+
"CAP_SYS_RAWIO", // 17
|
|
104
|
+
"CAP_SYS_CHROOT", // 18
|
|
105
|
+
"CAP_SYS_PTRACE", // 19
|
|
106
|
+
"CAP_SYS_PACCT", // 20
|
|
107
|
+
"CAP_SYS_ADMIN", // 21
|
|
108
|
+
"CAP_SYS_BOOT", // 22
|
|
109
|
+
"CAP_SYS_NICE", // 23
|
|
110
|
+
"CAP_SYS_RESOURCE", // 24
|
|
111
|
+
"CAP_SYS_TIME", // 25
|
|
112
|
+
"CAP_SYS_TTY_CONFIG", // 26
|
|
113
|
+
"CAP_MKNOD", // 27
|
|
114
|
+
"CAP_LEASE", // 28
|
|
115
|
+
"CAP_AUDIT_WRITE", // 29
|
|
116
|
+
"CAP_AUDIT_CONTROL", // 30
|
|
117
|
+
"CAP_SETFCAP", // 31
|
|
118
|
+
"CAP_MAC_OVERRIDE", // 32
|
|
119
|
+
"CAP_MAC_ADMIN", // 33
|
|
120
|
+
"CAP_SYSLOG", // 34
|
|
121
|
+
"CAP_WAKE_ALARM", // 35
|
|
122
|
+
"CAP_BLOCK_SUSPEND", // 36
|
|
123
|
+
"CAP_AUDIT_READ", // 37
|
|
124
|
+
"CAP_PERFMON", // 38
|
|
125
|
+
"CAP_BPF", // 39
|
|
126
|
+
"CAP_CHECKPOINT_RESTORE", // 40
|
|
127
|
+
];
|
|
128
|
+
// ── Helper: safe execFileSync wrapper ────────────────────────────────────────
|
|
129
|
+
/**
|
|
130
|
+
* Run a command synchronously with a timeout, returning stdout on success
|
|
131
|
+
* or `null` on any failure. Never throws.
|
|
132
|
+
*
|
|
133
|
+
* Uses execFileSafe which handles allowlist resolution and shell: false.
|
|
134
|
+
*/
|
|
135
|
+
function execSafe(file, args, timeoutMs = 5_000) {
|
|
136
|
+
try {
|
|
137
|
+
const result = execFileSafe(file, args, {
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
timeout: timeoutMs,
|
|
140
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ── PrivilegeManager ─────────────────────────────────────────────────────────
|
|
149
|
+
export class PrivilegeManager {
|
|
150
|
+
cachedStatus = null;
|
|
151
|
+
cacheExpiry = 0;
|
|
152
|
+
static CACHE_TTL = 30_000; // 30 seconds
|
|
153
|
+
static _instance = null;
|
|
154
|
+
constructor() {
|
|
155
|
+
// Singleton — use PrivilegeManager.instance()
|
|
156
|
+
}
|
|
157
|
+
/** Get or create the singleton instance. */
|
|
158
|
+
static instance() {
|
|
159
|
+
if (!PrivilegeManager._instance) {
|
|
160
|
+
PrivilegeManager._instance = new PrivilegeManager();
|
|
161
|
+
}
|
|
162
|
+
return PrivilegeManager._instance;
|
|
163
|
+
}
|
|
164
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Detect current privilege level.
|
|
167
|
+
* Results are cached for {@link CACHE_TTL} ms to avoid repeated
|
|
168
|
+
* subprocess spawns on rapid sequential tool calls.
|
|
169
|
+
*/
|
|
170
|
+
async getStatus() {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
if (this.cachedStatus && now < this.cacheExpiry) {
|
|
173
|
+
return this.cachedStatus;
|
|
174
|
+
}
|
|
175
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
|
|
176
|
+
const euid = typeof process.geteuid === "function" ? process.geteuid() : -1;
|
|
177
|
+
const isRoot = euid === 0;
|
|
178
|
+
const groups = this.getGroups();
|
|
179
|
+
const sudoAvailable = await this.isSudoAvailable();
|
|
180
|
+
const passwordlessSudo = sudoAvailable
|
|
181
|
+
? await this.testPasswordlessSudo()
|
|
182
|
+
: false;
|
|
183
|
+
const sudoSessionActive = SudoSession.getInstance().isElevated();
|
|
184
|
+
const capabilities = await this.getCurrentCapabilities();
|
|
185
|
+
const status = {
|
|
186
|
+
uid,
|
|
187
|
+
euid,
|
|
188
|
+
isRoot,
|
|
189
|
+
sudoAvailable,
|
|
190
|
+
passwordlessSudo,
|
|
191
|
+
sudoSessionActive,
|
|
192
|
+
capabilities,
|
|
193
|
+
groups,
|
|
194
|
+
};
|
|
195
|
+
this.cachedStatus = status;
|
|
196
|
+
this.cacheExpiry = now + PrivilegeManager.CACHE_TTL;
|
|
197
|
+
return status;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check whether a specific tool's privilege requirements are met.
|
|
201
|
+
*
|
|
202
|
+
* Evaluates the tool's `sudo` level and `capabilities` list against
|
|
203
|
+
* the current {@link PrivilegeStatus} and returns actionable issues.
|
|
204
|
+
*/
|
|
205
|
+
async checkForTool(manifest) {
|
|
206
|
+
const issues = [];
|
|
207
|
+
const recommendations = [];
|
|
208
|
+
// ── sudo: 'never' → always satisfied ───────────────────────────────
|
|
209
|
+
if (manifest.sudo === "never") {
|
|
210
|
+
return { satisfied: true, issues, recommendations };
|
|
211
|
+
}
|
|
212
|
+
const status = await this.getStatus();
|
|
213
|
+
// ── sudo: 'always' → must have root, session, passwordless sudo, or askpass ─
|
|
214
|
+
if (manifest.sudo === "always") {
|
|
215
|
+
const hasAskpass = isAskpassAvailable();
|
|
216
|
+
if (!status.isRoot && !status.sudoSessionActive && !status.passwordlessSudo && !hasAskpass) {
|
|
217
|
+
if (!status.sudoAvailable) {
|
|
218
|
+
issues.push({
|
|
219
|
+
type: "sudo-unavailable",
|
|
220
|
+
description: `Tool '${manifest.toolName}' requires elevated privileges but ` +
|
|
221
|
+
`the 'sudo' binary is not available on this system.`,
|
|
222
|
+
operation: manifest.toolName,
|
|
223
|
+
resolution: "Install sudo (e.g., 'apt install sudo') or run the server as root.",
|
|
224
|
+
});
|
|
225
|
+
recommendations.push("Install the 'sudo' package or run the MCP server as root.");
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const reason = manifest.sudoReason
|
|
229
|
+
? ` to ${manifest.sudoReason.charAt(0).toLowerCase()}${manifest.sudoReason.slice(1)}`
|
|
230
|
+
: "";
|
|
231
|
+
issues.push({
|
|
232
|
+
type: "sudo-required",
|
|
233
|
+
description: `Tool '${manifest.toolName}' requires elevated privileges${reason}. ` +
|
|
234
|
+
`No active sudo session or passwordless sudo detected.`,
|
|
235
|
+
operation: manifest.toolName,
|
|
236
|
+
resolution: "Call the 'sudo_elevate' tool first to provide your credentials for this session.",
|
|
237
|
+
});
|
|
238
|
+
recommendations.push(`Run the 'sudo_elevate' tool to provide credentials before using '${manifest.toolName}'.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// ── sudo: 'conditional' → don't block, but advise ──────────────────
|
|
243
|
+
if (manifest.sudo === "conditional") {
|
|
244
|
+
if (!status.isRoot &&
|
|
245
|
+
!status.sudoSessionActive &&
|
|
246
|
+
!status.passwordlessSudo &&
|
|
247
|
+
status.sudoAvailable) {
|
|
248
|
+
recommendations.push(`Tool '${manifest.toolName}' may show limited results without elevated privileges. ` +
|
|
249
|
+
`Consider running 'sudo_elevate' first for complete output.`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── Capability checks ──────────────────────────────────────────────
|
|
253
|
+
if (manifest.capabilities && manifest.capabilities.length > 0) {
|
|
254
|
+
for (const requiredCap of manifest.capabilities) {
|
|
255
|
+
const hasCap = status.capabilities.has(requiredCap);
|
|
256
|
+
// If running as root, have an active sudo session, passwordless sudo,
|
|
257
|
+
// or an askpass helper, capabilities will be available when the command
|
|
258
|
+
// runs under sudo, so don't flag.
|
|
259
|
+
const hasAskpassForCaps = isAskpassAvailable();
|
|
260
|
+
if (!hasCap && !status.isRoot && !status.sudoSessionActive && !status.passwordlessSudo && !hasAskpassForCaps) {
|
|
261
|
+
issues.push({
|
|
262
|
+
type: "capability-missing",
|
|
263
|
+
description: `Tool '${manifest.toolName}' requires Linux capability '${requiredCap}' ` +
|
|
264
|
+
`which is not in the current effective capability set.`,
|
|
265
|
+
operation: manifest.toolName,
|
|
266
|
+
resolution: `Either run 'sudo_elevate' to gain full privileges, or grant '${requiredCap}' ` +
|
|
267
|
+
`to the Node.js binary with: sudo setcap '${requiredCap.toLowerCase()}+ep' $(which node)`,
|
|
268
|
+
});
|
|
269
|
+
recommendations.push(`Capability '${requiredCap}' is required for '${manifest.toolName}'. ` +
|
|
270
|
+
`Use 'sudo_elevate' or grant the capability directly to the node binary.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
satisfied: issues.length === 0,
|
|
276
|
+
issues,
|
|
277
|
+
recommendations,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Check if a specific Linux capability is in the current effective set.
|
|
282
|
+
*/
|
|
283
|
+
async hasCapability(cap) {
|
|
284
|
+
const caps = await this.getCurrentCapabilities();
|
|
285
|
+
return caps.has(cap);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Parse the effective capability set from `/proc/self/status`.
|
|
289
|
+
*
|
|
290
|
+
* Reads the `CapEff` line which contains a hex-encoded bitmask,
|
|
291
|
+
* then maps set bits to capability names using the kernel-defined
|
|
292
|
+
* bit positions.
|
|
293
|
+
*/
|
|
294
|
+
async getCurrentCapabilities() {
|
|
295
|
+
const caps = new Set();
|
|
296
|
+
try {
|
|
297
|
+
const statusContent = readFileSync("/proc/self/status", "utf-8");
|
|
298
|
+
const capEffLine = statusContent
|
|
299
|
+
.split("\n")
|
|
300
|
+
.find((line) => line.startsWith("CapEff:"));
|
|
301
|
+
if (!capEffLine) {
|
|
302
|
+
return caps;
|
|
303
|
+
}
|
|
304
|
+
const hexStr = capEffLine.split(":")[1]?.trim();
|
|
305
|
+
if (!hexStr) {
|
|
306
|
+
return caps;
|
|
307
|
+
}
|
|
308
|
+
// Parse the hex string as a BigInt to handle the full 64-bit bitmask
|
|
309
|
+
const bitmask = BigInt("0x" + hexStr);
|
|
310
|
+
for (let bit = 0; bit < CAPABILITY_NAMES.length; bit++) {
|
|
311
|
+
if (bitmask & (1n << BigInt(bit))) {
|
|
312
|
+
caps.add(CAPABILITY_NAMES[bit]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// /proc/self/status may not exist on non-Linux systems;
|
|
318
|
+
// return empty set rather than crashing.
|
|
319
|
+
}
|
|
320
|
+
return caps;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Test whether passwordless sudo works by running `sudo -n true`.
|
|
324
|
+
* The `-n` (non-interactive) flag causes sudo to fail immediately
|
|
325
|
+
* rather than prompting if a password is required.
|
|
326
|
+
*/
|
|
327
|
+
async testPasswordlessSudo() {
|
|
328
|
+
const result = execSafe("sudo", ["-n", "true"], 5_000);
|
|
329
|
+
// execFileSync throws on non-zero exit, so if result is non-null it succeeded
|
|
330
|
+
return result !== null;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check whether the `sudo` binary exists on PATH.
|
|
334
|
+
*/
|
|
335
|
+
async isSudoAvailable() {
|
|
336
|
+
const result = execSafe("which", ["sudo"], 3_000);
|
|
337
|
+
return result !== null && result.trim().length > 0;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Invalidate the cached status.
|
|
341
|
+
* Should be called after events that change privilege state,
|
|
342
|
+
* e.g., after `sudo_elevate` or `sudo_drop`.
|
|
343
|
+
*/
|
|
344
|
+
clearCache() {
|
|
345
|
+
this.cachedStatus = null;
|
|
346
|
+
this.cacheExpiry = 0;
|
|
347
|
+
}
|
|
348
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
349
|
+
/**
|
|
350
|
+
* Get user group memberships via `id -Gn`.
|
|
351
|
+
* Returns an empty array on failure.
|
|
352
|
+
*/
|
|
353
|
+
getGroups() {
|
|
354
|
+
const output = execSafe("id", ["-Gn"], 3_000);
|
|
355
|
+
if (!output) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
return output
|
|
359
|
+
.trim()
|
|
360
|
+
.split(/\s+/)
|
|
361
|
+
.filter((g) => g.length > 0);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rate-limiter.ts — Token-bucket rate limiter for MCP tool invocations.
|
|
3
|
+
*
|
|
4
|
+
* Provides per-tool and global rate limiting to prevent abuse and resource
|
|
5
|
+
* exhaustion. Limits are configurable via environment variables.
|
|
6
|
+
*
|
|
7
|
+
* @module rate-limiter
|
|
8
|
+
* @see CICD-024
|
|
9
|
+
*/
|
|
10
|
+
/** Result of a rate limit check. */
|
|
11
|
+
export interface RateLimitResult {
|
|
12
|
+
/** Whether the invocation is allowed */
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
/** If rejected, the reason message */
|
|
15
|
+
reason?: string;
|
|
16
|
+
/** Remaining invocations in the current window (per-tool) */
|
|
17
|
+
remainingPerTool: number;
|
|
18
|
+
/** Remaining invocations in the current window (global) */
|
|
19
|
+
remainingGlobal: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Simple sliding-window rate limiter for tool invocations.
|
|
23
|
+
*
|
|
24
|
+
* Tracks invocations per tool and globally using timestamp arrays.
|
|
25
|
+
* Old entries outside the time window are pruned on each check.
|
|
26
|
+
*
|
|
27
|
+
* Configuration via environment variables:
|
|
28
|
+
* - `KALI_DEFENSE_RATE_LIMIT_PER_TOOL` — Max invocations per tool per window (default: 30)
|
|
29
|
+
* - `KALI_DEFENSE_RATE_LIMIT_GLOBAL` — Max total invocations per window (default: 100)
|
|
30
|
+
* - `KALI_DEFENSE_RATE_LIMIT_WINDOW` — Window size in seconds (default: 60)
|
|
31
|
+
*
|
|
32
|
+
* Set any limit to `0` to disable that particular limit.
|
|
33
|
+
*/
|
|
34
|
+
export declare class RateLimiter {
|
|
35
|
+
/** Per-tool invocation buckets */
|
|
36
|
+
private toolBuckets;
|
|
37
|
+
/** Global invocation bucket */
|
|
38
|
+
private globalBucket;
|
|
39
|
+
/** Max invocations per tool per window */
|
|
40
|
+
readonly maxPerTool: number;
|
|
41
|
+
/** Max total invocations per window */
|
|
42
|
+
readonly maxGlobal: number;
|
|
43
|
+
/** Window size in milliseconds */
|
|
44
|
+
readonly windowMs: number;
|
|
45
|
+
private static _instance;
|
|
46
|
+
constructor(maxPerTool?: number, maxGlobal?: number, windowMs?: number);
|
|
47
|
+
/** Get or create the singleton instance. */
|
|
48
|
+
static instance(): RateLimiter;
|
|
49
|
+
/** Reset the singleton (for testing). */
|
|
50
|
+
static resetInstance(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Check whether an invocation of `toolName` is allowed, and if so,
|
|
53
|
+
* record it. Returns a {@link RateLimitResult} indicating whether the
|
|
54
|
+
* call is permitted.
|
|
55
|
+
*
|
|
56
|
+
* @param toolName - The MCP tool name being invoked
|
|
57
|
+
* @returns Rate limit check result
|
|
58
|
+
*/
|
|
59
|
+
check(toolName: string): RateLimitResult;
|
|
60
|
+
/** Clear all rate limit state (for testing). */
|
|
61
|
+
reset(): void;
|
|
62
|
+
private getToolBucket;
|
|
63
|
+
private pruneGlobal;
|
|
64
|
+
private pruneTool;
|
|
65
|
+
private parseEnvInt;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rate-limiter.ts — Token-bucket rate limiter for MCP tool invocations.
|
|
3
|
+
*
|
|
4
|
+
* Provides per-tool and global rate limiting to prevent abuse and resource
|
|
5
|
+
* exhaustion. Limits are configurable via environment variables.
|
|
6
|
+
*
|
|
7
|
+
* @module rate-limiter
|
|
8
|
+
* @see CICD-024
|
|
9
|
+
*/
|
|
10
|
+
// ── Rate Limiter Class ───────────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Simple sliding-window rate limiter for tool invocations.
|
|
13
|
+
*
|
|
14
|
+
* Tracks invocations per tool and globally using timestamp arrays.
|
|
15
|
+
* Old entries outside the time window are pruned on each check.
|
|
16
|
+
*
|
|
17
|
+
* Configuration via environment variables:
|
|
18
|
+
* - `KALI_DEFENSE_RATE_LIMIT_PER_TOOL` — Max invocations per tool per window (default: 30)
|
|
19
|
+
* - `KALI_DEFENSE_RATE_LIMIT_GLOBAL` — Max total invocations per window (default: 100)
|
|
20
|
+
* - `KALI_DEFENSE_RATE_LIMIT_WINDOW` — Window size in seconds (default: 60)
|
|
21
|
+
*
|
|
22
|
+
* Set any limit to `0` to disable that particular limit.
|
|
23
|
+
*/
|
|
24
|
+
export class RateLimiter {
|
|
25
|
+
/** Per-tool invocation buckets */
|
|
26
|
+
toolBuckets = new Map();
|
|
27
|
+
/** Global invocation bucket */
|
|
28
|
+
globalBucket = { timestamps: [] };
|
|
29
|
+
/** Max invocations per tool per window */
|
|
30
|
+
maxPerTool;
|
|
31
|
+
/** Max total invocations per window */
|
|
32
|
+
maxGlobal;
|
|
33
|
+
/** Window size in milliseconds */
|
|
34
|
+
windowMs;
|
|
35
|
+
static _instance = null;
|
|
36
|
+
constructor(maxPerTool, maxGlobal, windowMs) {
|
|
37
|
+
this.maxPerTool = maxPerTool ?? this.parseEnvInt("KALI_DEFENSE_RATE_LIMIT_PER_TOOL", 30);
|
|
38
|
+
this.maxGlobal = maxGlobal ?? this.parseEnvInt("KALI_DEFENSE_RATE_LIMIT_GLOBAL", 100);
|
|
39
|
+
this.windowMs = (windowMs ?? this.parseEnvInt("KALI_DEFENSE_RATE_LIMIT_WINDOW", 60)) * 1000;
|
|
40
|
+
}
|
|
41
|
+
/** Get or create the singleton instance. */
|
|
42
|
+
static instance() {
|
|
43
|
+
if (!RateLimiter._instance) {
|
|
44
|
+
RateLimiter._instance = new RateLimiter();
|
|
45
|
+
}
|
|
46
|
+
return RateLimiter._instance;
|
|
47
|
+
}
|
|
48
|
+
/** Reset the singleton (for testing). */
|
|
49
|
+
static resetInstance() {
|
|
50
|
+
RateLimiter._instance = null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check whether an invocation of `toolName` is allowed, and if so,
|
|
54
|
+
* record it. Returns a {@link RateLimitResult} indicating whether the
|
|
55
|
+
* call is permitted.
|
|
56
|
+
*
|
|
57
|
+
* @param toolName - The MCP tool name being invoked
|
|
58
|
+
* @returns Rate limit check result
|
|
59
|
+
*/
|
|
60
|
+
check(toolName) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
// Prune expired entries
|
|
63
|
+
this.pruneGlobal(now);
|
|
64
|
+
this.pruneTool(toolName, now);
|
|
65
|
+
const toolBucket = this.getToolBucket(toolName);
|
|
66
|
+
const globalCount = this.globalBucket.timestamps.length;
|
|
67
|
+
const toolCount = toolBucket.timestamps.length;
|
|
68
|
+
// Check global limit (0 = disabled)
|
|
69
|
+
if (this.maxGlobal > 0 && globalCount >= this.maxGlobal) {
|
|
70
|
+
return {
|
|
71
|
+
allowed: false,
|
|
72
|
+
reason: `Global rate limit exceeded: ${globalCount}/${this.maxGlobal} invocations ` +
|
|
73
|
+
`in the last ${this.windowMs / 1000}s. Please wait before retrying.`,
|
|
74
|
+
remainingPerTool: Math.max(0, this.maxPerTool - toolCount),
|
|
75
|
+
remainingGlobal: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Check per-tool limit (0 = disabled)
|
|
79
|
+
if (this.maxPerTool > 0 && toolCount >= this.maxPerTool) {
|
|
80
|
+
return {
|
|
81
|
+
allowed: false,
|
|
82
|
+
reason: `Per-tool rate limit exceeded for '${toolName}': ${toolCount}/${this.maxPerTool} ` +
|
|
83
|
+
`invocations in the last ${this.windowMs / 1000}s. Please wait before retrying.`,
|
|
84
|
+
remainingPerTool: 0,
|
|
85
|
+
remainingGlobal: Math.max(0, this.maxGlobal - globalCount),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Allowed — record the invocation
|
|
89
|
+
toolBucket.timestamps.push(now);
|
|
90
|
+
this.globalBucket.timestamps.push(now);
|
|
91
|
+
return {
|
|
92
|
+
allowed: true,
|
|
93
|
+
remainingPerTool: this.maxPerTool > 0 ? this.maxPerTool - toolCount - 1 : Infinity,
|
|
94
|
+
remainingGlobal: this.maxGlobal > 0 ? this.maxGlobal - globalCount - 1 : Infinity,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Clear all rate limit state (for testing). */
|
|
98
|
+
reset() {
|
|
99
|
+
this.toolBuckets.clear();
|
|
100
|
+
this.globalBucket = { timestamps: [] };
|
|
101
|
+
}
|
|
102
|
+
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
103
|
+
getToolBucket(toolName) {
|
|
104
|
+
let bucket = this.toolBuckets.get(toolName);
|
|
105
|
+
if (!bucket) {
|
|
106
|
+
bucket = { timestamps: [] };
|
|
107
|
+
this.toolBuckets.set(toolName, bucket);
|
|
108
|
+
}
|
|
109
|
+
return bucket;
|
|
110
|
+
}
|
|
111
|
+
pruneGlobal(now) {
|
|
112
|
+
const cutoff = now - this.windowMs;
|
|
113
|
+
this.globalBucket.timestamps = this.globalBucket.timestamps.filter((t) => t > cutoff);
|
|
114
|
+
}
|
|
115
|
+
pruneTool(toolName, now) {
|
|
116
|
+
const bucket = this.toolBuckets.get(toolName);
|
|
117
|
+
if (bucket) {
|
|
118
|
+
const cutoff = now - this.windowMs;
|
|
119
|
+
bucket.timestamps = bucket.timestamps.filter((t) => t > cutoff);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
parseEnvInt(envVar, defaultValue) {
|
|
123
|
+
const raw = process.env[envVar];
|
|
124
|
+
if (raw === undefined)
|
|
125
|
+
return defaultValue;
|
|
126
|
+
const parsed = parseInt(raw, 10);
|
|
127
|
+
return isNaN(parsed) || parsed < 0 ? defaultValue : parsed;
|
|
128
|
+
}
|
|
129
|
+
}
|