defense-mcp-server 0.7.0 → 0.7.1

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 CHANGED
@@ -6,6 +6,43 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.7.1] — 2026-03-14
10
+
11
+ ### v0.7.1 — Critical PAM Hardening Fix
12
+
13
+ #### Security Fix
14
+ - **CRITICAL**: Fixed `pam_configure` action corrupting `/etc/pam.d/common-auth` — sed commands stripped whitespace separators between PAM fields, breaking ALL authentication system-wide (required GRUB recovery to fix)
15
+ - **CRITICAL**: Fixed `[success=N]` jump count not being updated after faillock rule insertion — would cause authentication denial on Debian/Ubuntu systems
16
+
17
+ #### New Module: `src/core/pam-utils.ts`
18
+ - Safe in-memory PAM config parser/serializer replacing fragile sed-based manipulation
19
+ - `parsePamConfig()` — Lossless parser handling comments, blanks, `@include`, bracket-style controls
20
+ - `serializePamConfig()` — Serializer with proper formatting (matching `pam-auth-update` canonical format)
21
+ - `validatePamConfig()` — Triple validation: field formatting, module existence, and `[success=N]` jump count correctness
22
+ - `adjustJumpCounts()` — Automatically updates bracket-control jump counts when rules are inserted/removed
23
+ - Manipulation helpers: `removeModuleRules()`, `insertBeforeModule()`, `insertAfterModule()` with pamType filter
24
+ - Sudo-aware I/O: `readPamFile()`, `writePamFile()` (atomic via `sudo install`), `backupPamFile()`, `restorePamFile()`
25
+
26
+ #### Safety Layers (Defense-in-Depth)
27
+ - Mandatory backup before any PAM file modification
28
+ - In-memory validation before writing (catches corrupted fields, missing pam_unix.so, wrong jump counts)
29
+ - Atomic file write using `sudo install -m 644 -o root -g root` (no partial state)
30
+ - Post-write re-read validation
31
+ - Auto-rollback on ANY failure (restores from backup automatically)
32
+
33
+ #### Security Review Remediations
34
+ - Fixed partial write state on chmod/chown failure (atomic `sudo install`)
35
+ - Fixed temp file symlink race (secure `mkdtempSync`)
36
+ - Fixed insert helpers matching by module only (added pamType filter)
37
+ - Fixed `backupPamFile` mutating BackupEntry internal state
38
+ - Fixed `restorePamFile` leaking PAM content to stdout via `tee`
39
+ - Added PAM modification warning to SafeguardRegistry
40
+
41
+ #### Testing
42
+ - 63 new tests in `tests/core/pam-utils.test.ts` covering parser, serializer, validator, jump count adjustment, manipulation helpers, and full faillock integration flow
43
+ - 7 new tests in `tests/tools/access-control.test.ts` for pam_configure regression testing
44
+ - Critical regression test: verifies concatenated PAM fields (the original lockout bug) can never be produced
45
+
9
46
  ## [0.7.0] — 2026-03-12
10
47
 
11
48
  ### v0.7.0 — Tool Consolidation & Sudo Hardening Overhaul
package/README.md CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  A Model Context Protocol (MCP) server that gives AI assistants access to **94 defensive security tools** on Linux. Connect it to Claude Desktop, Cursor, or any MCP-compatible client to harden systems, manage firewalls, scan for vulnerabilities, and enforce compliance — all through natural language conversation.
4
4
 
5
- ## What It Does
5
+ ## Why I Made This
6
+ Basically I'm a total noob when it comes to really serious system hardening so I thought I'd test the latest LLM models and see how far I could get. Turns out they're pretty helpful! I got tired of hardening my new systems by hand every time I spun up a new one so I made this MCP server to make it pretty easy. I jam packed as many security tools as I could into this thing so be prepared to burn tokens using it. Hopefully it helps you about half as much as its helped me.
6
7
 
7
- This server exposes Linux security tools as MCP tools that an AI assistant can invoke on your behalf. Instead of memorizing command syntax for dozens of security utilities, you describe what you want in plain English and the assistant calls the right tool with the right parameters.
8
+ ## So What It Does
8
9
 
9
- The 94 tools are organized into 32 modules:
10
+ This server exposes Linux security tools as MCP tools that an AI assistant can invoke on your behalf. Instead of memorizing command syntax for dozens of security utilities, you describe what you want in plain English and the assistant calls the right tool with the right parameters. Sounds pretty good right!
11
+
12
+ Here are the tools:
10
13
 
11
14
  | Module | What It Does |
12
15
  |--------|-------------|
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAqEA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAgFD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmNxB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAkFA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AA0FD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmNxB"}
@@ -1,5 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join as pathJoin } from "node:path";
3
5
  import { getConfig, getToolTimeout } from "./config.js";
4
6
  import { SudoSession } from "./sudo-session.js";
5
7
  import { SudoGuard } from "./sudo-guard.js";
@@ -8,14 +10,25 @@ import { resolveCommand, resolveSudoCommand } from "./command-allowlist.js";
8
10
  /**
9
11
  * Ordered list of known graphical sudo/SSH askpass helpers.
10
12
  * The first one found on the system will be used as the SUDO_ASKPASS program.
13
+ *
14
+ * NOTE: The user-local MCP askpass helper is checked first (highest priority)
15
+ * since it is installed as part of the native deployment setup and is the
16
+ * preferred path for desktop environments running the server natively.
17
+ * It is placed at ~/.local/bin/mcp-askpass with mode 0700 (owner-only).
11
18
  */
12
- const ASKPASS_CANDIDATES = [
13
- "/usr/bin/ssh-askpass", // Generic (often symlinked to ksshaskpass/gnome)
14
- "/usr/bin/ksshaskpass", // KDE Plasma
15
- "/usr/lib/ssh/x11-ssh-askpass", // X11 classic
16
- "/usr/libexec/openssh/gnome-ssh-askpass", // GNOME
17
- "/usr/bin/lxqt-sudo", // LXQt
18
- ];
19
+ function getAskpassCandidates() {
20
+ return [
21
+ // User-local MCP askpass wrapper (zenity/pinentry, installed by setup)
22
+ pathJoin(homedir(), ".local", "bin", "mcp-askpass"),
23
+ // System-level alternatives
24
+ "/usr/local/bin/mcp-askpass", // System-wide MCP askpass (if installed by admin)
25
+ "/usr/bin/ssh-askpass", // Generic (often symlinked to ksshaskpass/gnome)
26
+ "/usr/bin/ksshaskpass", // KDE Plasma
27
+ "/usr/lib/ssh/x11-ssh-askpass", // X11 classic
28
+ "/usr/libexec/openssh/gnome-ssh-askpass", // GNOME
29
+ "/usr/bin/lxqt-sudo", // LXQt
30
+ ];
31
+ }
19
32
  /** Cached result of askpass detection (null = not yet checked, undefined = not found) */
20
33
  let cachedAskpass = null;
21
34
  /**
@@ -43,14 +56,14 @@ function findAskpassHelper() {
43
56
  cachedAskpass = undefined;
44
57
  return undefined;
45
58
  }
46
- for (const candidate of ASKPASS_CANDIDATES) {
59
+ for (const candidate of getAskpassCandidates()) {
47
60
  if (existsSync(candidate)) {
48
61
  // SECURITY (CORE-016): Validate each candidate's ownership, permissions, and integrity
49
62
  const validation = SudoGuard.validateAskpassPath(candidate);
50
63
  if (validation.valid) {
51
64
  cachedAskpass = candidate;
52
65
  console.error(`[executor] Found verified askpass helper: ${candidate}`);
53
- return cachedAskpass;
66
+ return candidate;
54
67
  }
55
68
  console.error(`[executor] Skipping askpass candidate ${candidate}: ${validation.reason}`);
56
69
  }
@@ -77,8 +90,16 @@ function findAskpassHelper() {
77
90
  * `command: "sudo", args: ["iptables", ...]`.
78
91
  */
79
92
  function prepareSudoOptions(options) {
80
- // Only transform calls where the command is "sudo"
81
- if (options.command !== "sudo")
93
+ // FIX (ordering bug): By the time this function is called, the allowlist block
94
+ // in executeCommand() has already resolved "sudo" → "/usr/bin/sudo" (absolute
95
+ // path). The original guard `if (options.command !== "sudo")` would therefore
96
+ // always return early, permanently bypassing credential injection.
97
+ //
98
+ // Fix: accept both the bare name "sudo" (pre-allowlist, from tests or direct
99
+ // callers) and any absolute path ending in "/sudo" (post-allowlist resolution,
100
+ // the normal runtime path) so that injection works in both cases.
101
+ const isSudoCommand = options.command === "sudo" || options.command.endsWith("/sudo");
102
+ if (!isSudoCommand)
82
103
  return options;
83
104
  // Skip if the caller explicitly opted out (e.g. sudo-session.ts itself)
84
105
  if (options.skipSudoInjection)
@@ -60,7 +60,10 @@ export interface InstallResult {
60
60
  export declare const DEFENSIVE_TOOLS: ToolRequirement[];
61
61
  /**
62
62
  * Checks whether a tool binary is available on the system.
63
- * Uses `which` to find the binary, then attempts `--version` for version info.
63
+ * Uses the command allowlist (which already resolved paths via existsSync at
64
+ * startup) or falls back to probing standard binary directories with
65
+ * existsSync. This avoids shelling out to `which`, which is blocked by the
66
+ * command allowlist.
64
67
  */
65
68
  export declare function checkTool(binary: string): Promise<{
66
69
  installed: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/core/installer.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,QAAQ,GACR,gBAAgB,GAChB,YAAY,GACZ,WAAW,GACX,SAAS,GACT,WAAW,GACX,WAAW,GACX,YAAY,GACZ,SAAS,CAAC;AAEd;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,YAAY,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,oCAAoC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,eAAe,EAk6B5C,CAAC;AAEF;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsClE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,eAAe,EAAE,CAAC,CAkB5B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,aAAa,CAAC,CAwDxB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,QAAQ,CAAC,EAAE,YAAY,EACvB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,aAAa,EAAE,CAAC,CAgC1B"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/core/installer.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,QAAQ,GACR,gBAAgB,GAChB,YAAY,GACZ,WAAW,GACX,SAAS,GACT,WAAW,GACX,WAAW,GACX,YAAY,GACZ,SAAS,CAAC;AAEd;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,YAAY,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,oCAAoC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,eAAe,EAk6B5C,CAAC;AAYF;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA8ClE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,eAAe,EAAE,CAAC,CAkB5B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,aAAa,CAAC,CAwDxB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,QAAQ,CAAC,EAAE,YAAY,EACvB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,aAAa,EAAE,CAAC,CAgC1B"}
@@ -1,6 +1,8 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { executeCommand } from "./executor.js";
2
3
  import { detectDistro, getInstallCommand, getUpdateCommand, } from "./distro.js";
3
4
  import { getConfig } from "./config.js";
5
+ import { resolveCommand } from "./command-allowlist.js";
4
6
  /**
5
7
  * Comprehensive list of defensive security tools across categories.
6
8
  */
@@ -922,21 +924,43 @@ export const DEFENSIVE_TOOLS = [
922
924
  required: false,
923
925
  },
924
926
  ];
927
+ /** Standard binary directories to probe when a binary is not in the command allowlist. */
928
+ const STANDARD_BINARY_DIRS = [
929
+ "/usr/bin",
930
+ "/usr/sbin",
931
+ "/bin",
932
+ "/sbin",
933
+ "/usr/local/bin",
934
+ "/usr/local/sbin",
935
+ ];
925
936
  /**
926
937
  * Checks whether a tool binary is available on the system.
927
- * Uses `which` to find the binary, then attempts `--version` for version info.
938
+ * Uses the command allowlist (which already resolved paths via existsSync at
939
+ * startup) or falls back to probing standard binary directories with
940
+ * existsSync. This avoids shelling out to `which`, which is blocked by the
941
+ * command allowlist.
928
942
  */
929
943
  export async function checkTool(binary) {
930
- // Check if binary exists
931
- const whichResult = await executeCommand({
932
- command: "which",
933
- args: [binary],
934
- timeout: 5000,
935
- });
936
- if (whichResult.exitCode !== 0) {
944
+ // Attempt to resolve via the command allowlist first (O(1) lookup, already
945
+ // verified with existsSync at startup).
946
+ let binaryPath;
947
+ try {
948
+ binaryPath = resolveCommand(binary);
949
+ }
950
+ catch {
951
+ // Binary not in the allowlist or not yet resolved; fall back to probing
952
+ // standard directories with existsSync.
953
+ for (const dir of STANDARD_BINARY_DIRS) {
954
+ const candidate = `${dir}/${binary}`;
955
+ if (existsSync(candidate)) {
956
+ binaryPath = candidate;
957
+ break;
958
+ }
959
+ }
960
+ }
961
+ if (!binaryPath) {
937
962
  return { installed: false };
938
963
  }
939
- const binaryPath = whichResult.stdout.trim();
940
964
  // Try to get version
941
965
  let version;
942
966
  const versionResult = await executeCommand({
@@ -0,0 +1,233 @@
1
+ /**
2
+ * PAM configuration parser, serializer, validator, and file I/O manager.
3
+ *
4
+ * Replaces fragile sed-based PAM manipulation with safe in-memory operations:
5
+ * 1. Parse PAM config into structured records
6
+ * 2. Manipulate records (insert, remove, reorder)
7
+ * 3. Serialize back with correct formatting
8
+ * 4. Validate before writing
9
+ * 5. Write atomically with mandatory backup and auto-rollback
10
+ *
11
+ * @see docs/PAM-HARDENING-FIX.md for architecture details
12
+ */
13
+ import { type BackupEntry } from "./backup-manager.js";
14
+ /** A PAM rule line: type control module [args...] */
15
+ export interface PamRule {
16
+ kind: "rule";
17
+ /** PAM type: auth, account, password, session (optionally prefixed with -) */
18
+ pamType: string;
19
+ /** Control flag: required, requisite, sufficient, optional, or [value=action ...] */
20
+ control: string;
21
+ /** Module path/name: pam_unix.so, pam_faillock.so, etc. */
22
+ module: string;
23
+ /** Module arguments: nullok, silent, deny=5, etc. */
24
+ args: string[];
25
+ /** Original raw text (preserved for round-trip fidelity). */
26
+ rawLine: string;
27
+ }
28
+ /** A comment line (starts with #). */
29
+ export interface PamComment {
30
+ kind: "comment";
31
+ text: string;
32
+ }
33
+ /** A blank/empty line. */
34
+ export interface PamBlank {
35
+ kind: "blank";
36
+ }
37
+ /** An @include directive. */
38
+ export interface PamInclude {
39
+ kind: "include";
40
+ target: string;
41
+ rawLine: string;
42
+ }
43
+ /** Union of all PAM line types. */
44
+ export type PamLine = PamRule | PamComment | PamBlank | PamInclude;
45
+ /** Thrown when PAM config validation fails. */
46
+ export declare class PamValidationError extends Error {
47
+ readonly errors: string[];
48
+ readonly filePath?: string | undefined;
49
+ constructor(errors: string[], filePath?: string | undefined);
50
+ }
51
+ /** Thrown when PAM file write fails or post-write validation fails. */
52
+ export declare class PamWriteError extends Error {
53
+ readonly filePath: string;
54
+ readonly backupId?: string | undefined;
55
+ constructor(message: string, filePath: string, backupId?: string | undefined);
56
+ }
57
+ /**
58
+ * Parse PAM config file content into structured records.
59
+ *
60
+ * Handles:
61
+ * - Standard rules: auth required pam_unix.so nullok
62
+ * - Complex controls: auth [success=1 default=ignore] pam_unix.so
63
+ * - Comments: # This is a comment
64
+ * - Blank lines: (preserved for formatting fidelity)
65
+ * - Include directives: @include common-auth
66
+ *
67
+ * **Critical**: The parser is **lossless**. Every line in the input appears
68
+ * in the output array. Unknown/unparseable lines are preserved as comments
69
+ * to prevent silent data loss.
70
+ *
71
+ * @param content - Raw PAM config file text
72
+ * @returns Array of PamLine records in file order
73
+ */
74
+ export declare function parsePamConfig(content: string): PamLine[];
75
+ /**
76
+ * Serialize structured PAM records back to file content.
77
+ *
78
+ * For PamRule records, generates lines with consistent formatting:
79
+ * - Fields separated by 4-space padding
80
+ * - Module args separated by single spaces
81
+ *
82
+ * For PamComment, PamBlank, and PamInclude records, the original
83
+ * raw text is emitted unchanged (round-trip preservation).
84
+ *
85
+ * @param lines - Array of PamLine records
86
+ * @returns PAM config file content string (with trailing newline)
87
+ */
88
+ export declare function serializePamConfig(lines: PamLine[]): string;
89
+ /**
90
+ * Validate PAM config for syntactic correctness.
91
+ *
92
+ * Checks:
93
+ * 1. Every PamRule has a valid pamType, non-empty control, and module ending in .so
94
+ * 2. At least one pam_unix.so rule exists (sanity check — PAM needs it)
95
+ * 3. No lines have concatenated fields (the bug that caused the lockout)
96
+ *
97
+ * Does NOT check:
98
+ * - Whether .so files exist on disk
99
+ * - Semantic correctness of control flags
100
+ *
101
+ * @param lines - Parsed PamLine array
102
+ * @returns Validation result with error details
103
+ */
104
+ export declare function validatePamConfig(lines: PamLine[]): {
105
+ valid: boolean;
106
+ errors: string[];
107
+ };
108
+ /**
109
+ * Validate raw PAM config content string.
110
+ *
111
+ * Convenience wrapper that parses then validates.
112
+ *
113
+ * @param content - Raw PAM config file text
114
+ * @returns Validation result
115
+ */
116
+ export declare function validatePamConfigContent(content: string): {
117
+ valid: boolean;
118
+ errors: string[];
119
+ };
120
+ /**
121
+ * Create a new PamRule record.
122
+ *
123
+ * @param pamType - PAM type (auth, account, password, session)
124
+ * @param control - Control flag (required, requisite, [success=1 default=ignore], etc.)
125
+ * @param module - Module name (pam_faillock.so, pam_unix.so, etc.)
126
+ * @param args - Module arguments
127
+ * @returns New PamRule with generated rawLine
128
+ */
129
+ export declare function createPamRule(pamType: string, control: string, module: string, args: string[]): PamRule;
130
+ /**
131
+ * Remove all rules referencing a specific module.
132
+ *
133
+ * @param lines - Current PamLine array
134
+ * @param moduleName - Module to remove (e.g., "pam_faillock.so")
135
+ * @returns New array with matching rules removed
136
+ */
137
+ export declare function removeModuleRules(lines: PamLine[], moduleName: string): PamLine[];
138
+ /**
139
+ * Insert a new rule BEFORE the first rule matching targetModule.
140
+ * If targetModule is not found, appends at the end.
141
+ *
142
+ * @param lines - Current PamLine array
143
+ * @param targetModule - Module to insert before (e.g., "pam_unix.so")
144
+ * @param newRule - The rule to insert
145
+ * @param options - Optional filters: pamType restricts match to specific PAM type
146
+ * @returns New array with the rule inserted
147
+ */
148
+ export declare function insertBeforeModule(lines: PamLine[], targetModule: string, newRule: PamRule, options?: {
149
+ pamType?: string;
150
+ }): PamLine[];
151
+ /**
152
+ * Insert a new rule AFTER the first rule matching targetModule.
153
+ * If targetModule is not found, appends at the end.
154
+ *
155
+ * @param lines - Current PamLine array
156
+ * @param targetModule - Module to insert after (e.g., "pam_unix.so")
157
+ * @param newRule - The rule to insert
158
+ * @param options - Optional filters: pamType restricts match to specific PAM type
159
+ * @returns New array with the rule inserted
160
+ */
161
+ export declare function insertAfterModule(lines: PamLine[], targetModule: string, newRule: PamRule, options?: {
162
+ pamType?: string;
163
+ }): PamLine[];
164
+ /**
165
+ * Find all rules referencing a specific module.
166
+ *
167
+ * @param lines - PamLine array to search
168
+ * @param moduleName - Module to find (e.g., "pam_faillock.so")
169
+ * @returns Array of matching PamRule records
170
+ */
171
+ export declare function findModuleRules(lines: PamLine[], moduleName: string): PamRule[];
172
+ /**
173
+ * After inserting rules, adjust [success=N] jump counts on any rule
174
+ * that uses bracket-style controls with a success=N pattern.
175
+ *
176
+ * For each rule with [success=N ...], count how many rules now exist
177
+ * between that rule and pam_deny.so (requisite), and update N so that
178
+ * success still jumps PAST pam_deny.so.
179
+ *
180
+ * @param lines - PamLine array (typically after insertions)
181
+ * @returns New array with corrected jump counts
182
+ */
183
+ export declare function adjustJumpCounts(lines: PamLine[]): PamLine[];
184
+ /**
185
+ * Read a PAM config file via sudo.
186
+ *
187
+ * @param filePath - Absolute path (e.g., /etc/pam.d/common-auth)
188
+ * @returns File content string
189
+ * @throws If sudo cat fails
190
+ */
191
+ export declare function readPamFile(filePath: string): Promise<string>;
192
+ /**
193
+ * Write a PAM config file via sudo, with mandatory pre-write validation.
194
+ *
195
+ * Steps:
196
+ * 1. Parse the content with parsePamConfig()
197
+ * 2. Validate with validatePamConfig() — if invalid, throw (never write bad content)
198
+ * 3. Write to a secure temp directory (mkdtempSync — eliminates symlink race)
199
+ * 4. Use `sudo install -m 644 -o root -g root` for atomic write (eliminates partial-write state)
200
+ * 5. Post-write verification
201
+ *
202
+ * @param filePath - Absolute path
203
+ * @param content - PAM config content to write
204
+ * @throws PamValidationError if pre-write validation fails
205
+ * @throws PamWriteError if write or permission setting fails
206
+ */
207
+ export declare function writePamFile(filePath: string, content: string): Promise<void>;
208
+ /**
209
+ * Backup a PAM file using the project BackupManager.
210
+ *
211
+ * Since PAM files are root-owned, this:
212
+ * 1. Reads content via sudo cat
213
+ * 2. Writes to a secure temp directory (eliminates symlink race)
214
+ * 3. Uses BackupManager.backupSync() to create a tracked backup
215
+ * 4. Returns a new object (does NOT mutate BackupManager's internal entry)
216
+ * 5. Cleans up the temp file/directory
217
+ *
218
+ * @param filePath - PAM file to backup
219
+ * @returns BackupEntry for later restore (with corrected originalPath)
220
+ */
221
+ export declare function backupPamFile(filePath: string): Promise<BackupEntry>;
222
+ /**
223
+ * Restore a PAM file from backup.
224
+ *
225
+ * 1. Reads backup content from BackupManager's directory
226
+ * 2. Validates the backup content (refuse to restore garbage)
227
+ * 3. Writes to a secure temp file, then uses `sudo install` (eliminates tee stdout leak)
228
+ *
229
+ * @param backupEntry - The BackupEntry from backupPamFile()
230
+ * @throws If backup file is missing, invalid, or restore fails
231
+ */
232
+ export declare function restorePamFile(backupEntry: BackupEntry): Promise<void>;
233
+ //# sourceMappingURL=pam-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pam-utils.d.ts","sourceRoot":"","sources":["../../src/core/pam-utils.ts"],"names":[],"mappings":"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"}