defense-mcp-server 0.8.0 → 0.8.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.
|
@@ -230,4 +230,127 @@ export declare function backupPamFile(filePath: string): Promise<BackupEntry>;
|
|
|
230
230
|
* @throws If backup file is missing, invalid, or restore fails
|
|
231
231
|
*/
|
|
232
232
|
export declare function restorePamFile(backupEntry: BackupEntry): Promise<void>;
|
|
233
|
+
/** A single finding from PAM policy sanity validation. */
|
|
234
|
+
export interface PamSanityFinding {
|
|
235
|
+
/** warning = proceed with caution; critical = blocks operation unless forced */
|
|
236
|
+
severity: "warning" | "critical";
|
|
237
|
+
/** Which module the finding relates to */
|
|
238
|
+
module: "pam_faillock.so" | "pam_pwquality.so" | "general";
|
|
239
|
+
/** The specific parameter that triggered the finding, if applicable */
|
|
240
|
+
parameter?: string;
|
|
241
|
+
/** The problematic value */
|
|
242
|
+
value?: string | number;
|
|
243
|
+
/** Human-readable description of the problem */
|
|
244
|
+
message: string;
|
|
245
|
+
/** What the user should do instead */
|
|
246
|
+
recommendation: string;
|
|
247
|
+
}
|
|
248
|
+
/** Result of PAM policy sanity validation. */
|
|
249
|
+
export interface PamSanityResult {
|
|
250
|
+
/** true if no critical findings exist */
|
|
251
|
+
safe: boolean;
|
|
252
|
+
/** All findings, ordered by severity then module */
|
|
253
|
+
findings: PamSanityFinding[];
|
|
254
|
+
/** Count of critical-severity findings */
|
|
255
|
+
criticalCount: number;
|
|
256
|
+
/** Count of warning-severity findings */
|
|
257
|
+
warningCount: number;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Thresholds for PAM policy sanity checks.
|
|
261
|
+
* These define what constitutes "sane" vs "dangerous" PAM policy values.
|
|
262
|
+
* Tuned to prevent lockouts while allowing reasonable security hardening.
|
|
263
|
+
*/
|
|
264
|
+
export declare const PAM_SANITY_THRESHOLDS: {
|
|
265
|
+
readonly faillock: {
|
|
266
|
+
/** deny below this triggers critical — too few attempts before lockout */
|
|
267
|
+
readonly minDeny: 3;
|
|
268
|
+
/** unlock_time above this triggers warning — extended lockout */
|
|
269
|
+
readonly maxUnlockTimeWarn: 1800;
|
|
270
|
+
/** unlock_time above this triggers critical — extreme lockout */
|
|
271
|
+
readonly maxUnlockTimeCritical: 86400;
|
|
272
|
+
/** fail_interval below this triggers warning — unusually short window */
|
|
273
|
+
readonly minFailInterval: 60;
|
|
274
|
+
};
|
|
275
|
+
readonly pwquality: {
|
|
276
|
+
/** minlen above this triggers warning — unusually long */
|
|
277
|
+
readonly maxMinlenWarn: 24;
|
|
278
|
+
/** minlen above this triggers critical — unreasonably long */
|
|
279
|
+
readonly maxMinlenCritical: 64;
|
|
280
|
+
/** retry below this triggers critical — no second chance */
|
|
281
|
+
readonly minRetry: 2;
|
|
282
|
+
/** Combined credit threshold: all credits at this or below with high minlen */
|
|
283
|
+
readonly restrictiveCreditThreshold: -2;
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Validate faillock parameters for policy sanity.
|
|
288
|
+
*
|
|
289
|
+
* Checks for overly restrictive settings that could cause lockouts:
|
|
290
|
+
* - deny too low (typos cause lockout)
|
|
291
|
+
* - unlock_time too high or zero (extended/permanent lockout)
|
|
292
|
+
* - deny + unlock_time=0 combination (permanent lock on typos)
|
|
293
|
+
* - fail_interval too short
|
|
294
|
+
*
|
|
295
|
+
* @param params - Faillock parameters to validate
|
|
296
|
+
* @returns Array of sanity findings (empty = all sane)
|
|
297
|
+
*/
|
|
298
|
+
export declare function validateFaillockParams(params: {
|
|
299
|
+
deny?: number;
|
|
300
|
+
unlock_time?: number;
|
|
301
|
+
fail_interval?: number;
|
|
302
|
+
}): PamSanityFinding[];
|
|
303
|
+
/**
|
|
304
|
+
* Validate pwquality parameters for policy sanity.
|
|
305
|
+
*
|
|
306
|
+
* Checks for overly restrictive settings that prevent password creation:
|
|
307
|
+
* - minlen too high
|
|
308
|
+
* - retry too low (no second chance)
|
|
309
|
+
* - All character class requirements simultaneously very strict
|
|
310
|
+
*
|
|
311
|
+
* @param params - Pwquality parameters to validate
|
|
312
|
+
* @returns Array of sanity findings (empty = all sane)
|
|
313
|
+
*/
|
|
314
|
+
export declare function validatePwqualityParams(params: {
|
|
315
|
+
minlen?: number;
|
|
316
|
+
dcredit?: number;
|
|
317
|
+
ucredit?: number;
|
|
318
|
+
lcredit?: number;
|
|
319
|
+
ocredit?: number;
|
|
320
|
+
minclass?: number;
|
|
321
|
+
maxrepeat?: number;
|
|
322
|
+
retry?: number;
|
|
323
|
+
}): PamSanityFinding[];
|
|
324
|
+
/**
|
|
325
|
+
* Validate a PAM config structure for dangerous patterns.
|
|
326
|
+
*
|
|
327
|
+
* Checks the resulting PamLine[] after manipulation for patterns
|
|
328
|
+
* that would break authentication:
|
|
329
|
+
* - pam_deny.so as first auth rule (blocks all auth)
|
|
330
|
+
* - Missing pam_unix.so in auth stack
|
|
331
|
+
* - Incomplete faillock setup (preauth without authfail or vice versa)
|
|
332
|
+
* - Missing pam_permit.so in session stack
|
|
333
|
+
*
|
|
334
|
+
* @param lines - Parsed PAM config lines (after manipulation)
|
|
335
|
+
* @returns Array of sanity findings
|
|
336
|
+
*/
|
|
337
|
+
export declare function validatePamConfigSanity(lines: PamLine[]): PamSanityFinding[];
|
|
338
|
+
/**
|
|
339
|
+
* Validate PAM policy sanity — combined parameter + config check.
|
|
340
|
+
*
|
|
341
|
+
* This is the main entry point for sanity validation. It runs:
|
|
342
|
+
* 1. Module-specific parameter checks (if module + params provided)
|
|
343
|
+
* 2. Config structure checks (if lines provided)
|
|
344
|
+
*
|
|
345
|
+
* @param options - What to validate
|
|
346
|
+
* @returns Combined sanity result with safe flag and all findings
|
|
347
|
+
*/
|
|
348
|
+
export declare function validatePamPolicySanity(options: {
|
|
349
|
+
/** Which PAM module is being configured */
|
|
350
|
+
module?: "faillock" | "pwquality";
|
|
351
|
+
/** Module parameters being applied */
|
|
352
|
+
params?: Record<string, unknown>;
|
|
353
|
+
/** Resulting PAM config lines (after manipulation) */
|
|
354
|
+
lines?: PamLine[];
|
|
355
|
+
}): PamSanityResult;
|
|
233
356
|
//# sourceMappingURL=pam-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pam-utils.d.ts","sourceRoot":"","sources":["../../src/core/pam-utils.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pam-utils.d.ts","sourceRoot":"","sources":["../../src/core/pam-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAKtE,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0BAA0B;AAC1B,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6BAA6B;AAC7B,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mCAAmC;AACnC,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,CAAC;AAInE,+CAA+C;AAC/C,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,MAAM,EAAE,MAAM,EAAE;aAChB,QAAQ,CAAC,EAAE,MAAM;gBADjB,MAAM,EAAE,MAAM,EAAE,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAOpC;AAED,uEAAuE;AACvE,qBAAa,aAAc,SAAQ,KAAK;aAGpB,QAAQ,EAAE,MAAM;aAChB,QAAQ,CAAC,EAAE,MAAM;gBAFjC,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAKpC;AA+BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CA2CzD;AAqDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAyB3D;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EAAE,GACf;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA0FtC;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,GACd;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAGtC;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAWT;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,EAAE,CAIX;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EAAE,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,OAAO,EAAE,CAgBX;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EAAE,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,OAAO,EAAE,CAgBX;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,EAAE,CAKX;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CA2D5D;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAcnE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAqDf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CAyCtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,IAAI,CAAC,CAqDf;AAID,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,EAAE,SAAS,GAAG,UAAU,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,iBAAiB,GAAG,kBAAkB,GAAG,SAAS,CAAC;IAC3D,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,eAAe;IAC9B,yCAAyC;IACzC,IAAI,EAAE,OAAO,CAAC;IACd,oDAAoD;IACpD,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;CACtB;AAID;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;QAE9B,0EAA0E;;QAE1E,iEAAiE;;QAEjE,iEAAiE;;QAEjE,yEAAyE;;;;QAIzE,0DAA0D;;QAE1D,8DAA8D;;QAE9D,4DAA4D;;QAE5D,+EAA+E;;;CAGzE,CAAC;AAIX;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,gBAAgB,EAAE,CAgErB;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,gBAAgB,EAAE,CA2ErB;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,gBAAgB,EAAE,CAoE5E;AAID;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,2CAA2C;IAC3C,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;IAClC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,sDAAsD;IACtD,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;CACnB,GAAG,eAAe,CAmDlB"}
|
package/build/core/pam-utils.js
CHANGED
|
@@ -624,3 +624,312 @@ export async function restorePamFile(backupEntry) {
|
|
|
624
624
|
}
|
|
625
625
|
console.error(`[pam-utils] Restored ${backupEntry.backupPath} → ${backupEntry.originalPath}`);
|
|
626
626
|
}
|
|
627
|
+
// ── PAM Sanity Thresholds ───────────────────────────────────────────────────
|
|
628
|
+
/**
|
|
629
|
+
* Thresholds for PAM policy sanity checks.
|
|
630
|
+
* These define what constitutes "sane" vs "dangerous" PAM policy values.
|
|
631
|
+
* Tuned to prevent lockouts while allowing reasonable security hardening.
|
|
632
|
+
*/
|
|
633
|
+
export const PAM_SANITY_THRESHOLDS = {
|
|
634
|
+
faillock: {
|
|
635
|
+
/** deny below this triggers critical — too few attempts before lockout */
|
|
636
|
+
minDeny: 3,
|
|
637
|
+
/** unlock_time above this triggers warning — extended lockout */
|
|
638
|
+
maxUnlockTimeWarn: 1800, // 30 minutes
|
|
639
|
+
/** unlock_time above this triggers critical — extreme lockout */
|
|
640
|
+
maxUnlockTimeCritical: 86400, // 24 hours
|
|
641
|
+
/** fail_interval below this triggers warning — unusually short window */
|
|
642
|
+
minFailInterval: 60, // 1 minute
|
|
643
|
+
},
|
|
644
|
+
pwquality: {
|
|
645
|
+
/** minlen above this triggers warning — unusually long */
|
|
646
|
+
maxMinlenWarn: 24,
|
|
647
|
+
/** minlen above this triggers critical — unreasonably long */
|
|
648
|
+
maxMinlenCritical: 64,
|
|
649
|
+
/** retry below this triggers critical — no second chance */
|
|
650
|
+
minRetry: 2,
|
|
651
|
+
/** Combined credit threshold: all credits at this or below with high minlen */
|
|
652
|
+
restrictiveCreditThreshold: -2,
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
// ── Faillock Parameter Validation ───────────────────────────────────────────
|
|
656
|
+
/**
|
|
657
|
+
* Validate faillock parameters for policy sanity.
|
|
658
|
+
*
|
|
659
|
+
* Checks for overly restrictive settings that could cause lockouts:
|
|
660
|
+
* - deny too low (typos cause lockout)
|
|
661
|
+
* - unlock_time too high or zero (extended/permanent lockout)
|
|
662
|
+
* - deny + unlock_time=0 combination (permanent lock on typos)
|
|
663
|
+
* - fail_interval too short
|
|
664
|
+
*
|
|
665
|
+
* @param params - Faillock parameters to validate
|
|
666
|
+
* @returns Array of sanity findings (empty = all sane)
|
|
667
|
+
*/
|
|
668
|
+
export function validateFaillockParams(params) {
|
|
669
|
+
const findings = [];
|
|
670
|
+
const T = PAM_SANITY_THRESHOLDS.faillock;
|
|
671
|
+
// Check deny
|
|
672
|
+
if (params.deny !== undefined && params.deny < T.minDeny) {
|
|
673
|
+
findings.push({
|
|
674
|
+
severity: "critical",
|
|
675
|
+
module: "pam_faillock.so",
|
|
676
|
+
parameter: "deny",
|
|
677
|
+
value: params.deny,
|
|
678
|
+
message: params.deny === 1
|
|
679
|
+
? `deny=${params.deny}: A single failed attempt locks the account — typos cause immediate lockout`
|
|
680
|
+
: `deny=${params.deny}: Only ${params.deny} attempts before lockout — insufficient margin for typos`,
|
|
681
|
+
recommendation: `Set deny >= ${T.minDeny} (CIS Benchmark recommends 3-5)`,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// Check unlock_time
|
|
685
|
+
if (params.unlock_time !== undefined) {
|
|
686
|
+
if (params.unlock_time === 0) {
|
|
687
|
+
findings.push({
|
|
688
|
+
severity: "critical",
|
|
689
|
+
module: "pam_faillock.so",
|
|
690
|
+
parameter: "unlock_time",
|
|
691
|
+
value: 0,
|
|
692
|
+
message: "unlock_time=0: Permanent lock until admin runs 'faillock --reset' — no automatic recovery",
|
|
693
|
+
recommendation: "Set unlock_time to a positive value (e.g., 900 for 15 minutes)",
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else if (params.unlock_time > T.maxUnlockTimeCritical) {
|
|
697
|
+
findings.push({
|
|
698
|
+
severity: "critical",
|
|
699
|
+
module: "pam_faillock.so",
|
|
700
|
+
parameter: "unlock_time",
|
|
701
|
+
value: params.unlock_time,
|
|
702
|
+
message: `unlock_time=${params.unlock_time}: Lockout exceeds 24 hours — effectively permanent for most users`,
|
|
703
|
+
recommendation: `Set unlock_time <= ${T.maxUnlockTimeWarn} (30 minutes, per CIS Benchmark)`,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
else if (params.unlock_time > T.maxUnlockTimeWarn) {
|
|
707
|
+
findings.push({
|
|
708
|
+
severity: "warning",
|
|
709
|
+
module: "pam_faillock.so",
|
|
710
|
+
parameter: "unlock_time",
|
|
711
|
+
value: params.unlock_time,
|
|
712
|
+
message: `unlock_time=${params.unlock_time}: Lockout exceeds 30 minutes — consider a shorter unlock time`,
|
|
713
|
+
recommendation: `Set unlock_time <= ${T.maxUnlockTimeWarn} (30 minutes, per CIS Benchmark)`,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Check fail_interval
|
|
718
|
+
if (params.fail_interval !== undefined && params.fail_interval < T.minFailInterval) {
|
|
719
|
+
findings.push({
|
|
720
|
+
severity: "warning",
|
|
721
|
+
module: "pam_faillock.so",
|
|
722
|
+
parameter: "fail_interval",
|
|
723
|
+
value: params.fail_interval,
|
|
724
|
+
message: `fail_interval=${params.fail_interval}: Very short failure tracking window (< 60s)`,
|
|
725
|
+
recommendation: `Set fail_interval >= ${T.minFailInterval} (60 seconds or more)`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return findings;
|
|
729
|
+
}
|
|
730
|
+
// ── Pwquality Parameter Validation ──────────────────────────────────────────
|
|
731
|
+
/**
|
|
732
|
+
* Validate pwquality parameters for policy sanity.
|
|
733
|
+
*
|
|
734
|
+
* Checks for overly restrictive settings that prevent password creation:
|
|
735
|
+
* - minlen too high
|
|
736
|
+
* - retry too low (no second chance)
|
|
737
|
+
* - All character class requirements simultaneously very strict
|
|
738
|
+
*
|
|
739
|
+
* @param params - Pwquality parameters to validate
|
|
740
|
+
* @returns Array of sanity findings (empty = all sane)
|
|
741
|
+
*/
|
|
742
|
+
export function validatePwqualityParams(params) {
|
|
743
|
+
const findings = [];
|
|
744
|
+
const T = PAM_SANITY_THRESHOLDS.pwquality;
|
|
745
|
+
// Check minlen
|
|
746
|
+
if (params.minlen !== undefined) {
|
|
747
|
+
if (params.minlen > T.maxMinlenCritical) {
|
|
748
|
+
findings.push({
|
|
749
|
+
severity: "critical",
|
|
750
|
+
module: "pam_pwquality.so",
|
|
751
|
+
parameter: "minlen",
|
|
752
|
+
value: params.minlen,
|
|
753
|
+
message: `minlen=${params.minlen}: Minimum password length exceeds ${T.maxMinlenCritical} — users cannot create compliant passwords`,
|
|
754
|
+
recommendation: `Set minlen <= ${T.maxMinlenWarn} (NIST SP 800-63B recommends 8-64 characters)`,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
else if (params.minlen > T.maxMinlenWarn) {
|
|
758
|
+
findings.push({
|
|
759
|
+
severity: "warning",
|
|
760
|
+
module: "pam_pwquality.so",
|
|
761
|
+
parameter: "minlen",
|
|
762
|
+
value: params.minlen,
|
|
763
|
+
message: `minlen=${params.minlen}: Minimum password length exceeds ${T.maxMinlenWarn} — may be difficult for users`,
|
|
764
|
+
recommendation: `Set minlen <= ${T.maxMinlenWarn} for usability (14-16 is a good balance)`,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Check retry
|
|
769
|
+
if (params.retry !== undefined && params.retry < T.minRetry) {
|
|
770
|
+
findings.push({
|
|
771
|
+
severity: "critical",
|
|
772
|
+
module: "pam_pwquality.so",
|
|
773
|
+
parameter: "retry",
|
|
774
|
+
value: params.retry,
|
|
775
|
+
message: params.retry === 0
|
|
776
|
+
? "retry=0: Zero retries — password rejected on first attempt with no recovery"
|
|
777
|
+
: `retry=${params.retry}: Only ${params.retry} retry — insufficient for correcting typos`,
|
|
778
|
+
recommendation: `Set retry >= ${T.minRetry}`,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
// Check combined restrictive credit requirements with high minlen
|
|
782
|
+
const minlen = params.minlen ?? 0;
|
|
783
|
+
const credits = [params.dcredit, params.ucredit, params.lcredit, params.ocredit];
|
|
784
|
+
const definedCredits = credits.filter((c) => c !== undefined);
|
|
785
|
+
if (definedCredits.length === 4 && minlen > 16) {
|
|
786
|
+
const allVeryRestrictive = definedCredits.every((c) => c <= T.restrictiveCreditThreshold);
|
|
787
|
+
if (allVeryRestrictive) {
|
|
788
|
+
findings.push({
|
|
789
|
+
severity: "warning",
|
|
790
|
+
module: "pam_pwquality.so",
|
|
791
|
+
parameter: "dcredit+ucredit+lcredit+ocredit",
|
|
792
|
+
message: `All character classes require ${Math.abs(T.restrictiveCreditThreshold)}+ characters with minlen=${minlen} — very restrictive combined requirements`,
|
|
793
|
+
recommendation: "Relax either the character class requirements or the minimum length",
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Check minclass=4 with high minlen
|
|
798
|
+
if (params.minclass !== undefined && params.minclass >= 4 && minlen > 16) {
|
|
799
|
+
findings.push({
|
|
800
|
+
severity: "warning",
|
|
801
|
+
module: "pam_pwquality.so",
|
|
802
|
+
parameter: "minclass",
|
|
803
|
+
value: params.minclass,
|
|
804
|
+
message: `minclass=${params.minclass} with minlen=${minlen}: All ${params.minclass} character classes required with long minimum — very restrictive`,
|
|
805
|
+
recommendation: "Consider minclass=3 or reducing minlen when requiring all character classes",
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return findings;
|
|
809
|
+
}
|
|
810
|
+
// ── PAM Config Structure Validation ─────────────────────────────────────────
|
|
811
|
+
/**
|
|
812
|
+
* Validate a PAM config structure for dangerous patterns.
|
|
813
|
+
*
|
|
814
|
+
* Checks the resulting PamLine[] after manipulation for patterns
|
|
815
|
+
* that would break authentication:
|
|
816
|
+
* - pam_deny.so as first auth rule (blocks all auth)
|
|
817
|
+
* - Missing pam_unix.so in auth stack
|
|
818
|
+
* - Incomplete faillock setup (preauth without authfail or vice versa)
|
|
819
|
+
* - Missing pam_permit.so in session stack
|
|
820
|
+
*
|
|
821
|
+
* @param lines - Parsed PAM config lines (after manipulation)
|
|
822
|
+
* @returns Array of sanity findings
|
|
823
|
+
*/
|
|
824
|
+
export function validatePamConfigSanity(lines) {
|
|
825
|
+
const findings = [];
|
|
826
|
+
const authRules = lines.filter((l) => l.kind === "rule" && (l.pamType === "auth" || l.pamType === "-auth"));
|
|
827
|
+
const sessionRules = lines.filter((l) => l.kind === "rule" && (l.pamType === "session" || l.pamType === "-session"));
|
|
828
|
+
// Check: pam_deny.so as first auth rule
|
|
829
|
+
if (authRules.length > 0 && authRules[0].module === "pam_deny.so") {
|
|
830
|
+
findings.push({
|
|
831
|
+
severity: "critical",
|
|
832
|
+
module: "general",
|
|
833
|
+
message: "pam_deny.so is the first auth rule — this blocks ALL authentication",
|
|
834
|
+
recommendation: "Ensure pam_deny.so is not the first rule in the auth stack",
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
// Check: no pam_unix.so in auth stack
|
|
838
|
+
const hasUnixAuth = authRules.some((r) => r.module === "pam_unix.so");
|
|
839
|
+
if (authRules.length > 0 && !hasUnixAuth) {
|
|
840
|
+
findings.push({
|
|
841
|
+
severity: "critical",
|
|
842
|
+
module: "general",
|
|
843
|
+
message: "No pam_unix.so in auth stack — basic password authentication is broken",
|
|
844
|
+
recommendation: "Ensure pam_unix.so is present in the auth stack",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
// Check: incomplete faillock setup
|
|
848
|
+
const faillockRules = authRules.filter((r) => r.module === "pam_faillock.so");
|
|
849
|
+
if (faillockRules.length > 0) {
|
|
850
|
+
const hasPreauth = faillockRules.some((r) => r.args.includes("preauth"));
|
|
851
|
+
const hasAuthfail = faillockRules.some((r) => r.args.includes("authfail"));
|
|
852
|
+
if (hasPreauth && !hasAuthfail) {
|
|
853
|
+
findings.push({
|
|
854
|
+
severity: "warning",
|
|
855
|
+
module: "pam_faillock.so",
|
|
856
|
+
message: "Incomplete faillock setup: preauth rule present but authfail rule missing — failed attempts may not be tracked",
|
|
857
|
+
recommendation: "Add a pam_faillock.so authfail rule after pam_unix.so",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (hasAuthfail && !hasPreauth) {
|
|
861
|
+
findings.push({
|
|
862
|
+
severity: "warning",
|
|
863
|
+
module: "pam_faillock.so",
|
|
864
|
+
message: "Incomplete faillock setup: authfail rule present but preauth rule missing — locked accounts may not be checked before authentication",
|
|
865
|
+
recommendation: "Add a pam_faillock.so preauth rule before pam_unix.so",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Check: missing pam_permit.so in session stack
|
|
870
|
+
const hasPermitSession = sessionRules.some((r) => r.module === "pam_permit.so");
|
|
871
|
+
if (sessionRules.length > 0 && !hasPermitSession) {
|
|
872
|
+
findings.push({
|
|
873
|
+
severity: "warning",
|
|
874
|
+
module: "general",
|
|
875
|
+
message: "No pam_permit.so in session stack — sessions may fail to initialize",
|
|
876
|
+
recommendation: "Add 'session required pam_permit.so' to the session stack",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return findings;
|
|
880
|
+
}
|
|
881
|
+
// ── Combined Entry Point ────────────────────────────────────────────────────
|
|
882
|
+
/**
|
|
883
|
+
* Validate PAM policy sanity — combined parameter + config check.
|
|
884
|
+
*
|
|
885
|
+
* This is the main entry point for sanity validation. It runs:
|
|
886
|
+
* 1. Module-specific parameter checks (if module + params provided)
|
|
887
|
+
* 2. Config structure checks (if lines provided)
|
|
888
|
+
*
|
|
889
|
+
* @param options - What to validate
|
|
890
|
+
* @returns Combined sanity result with safe flag and all findings
|
|
891
|
+
*/
|
|
892
|
+
export function validatePamPolicySanity(options) {
|
|
893
|
+
const findings = [];
|
|
894
|
+
// 1. Module-specific parameter checks
|
|
895
|
+
if (options.module && options.params) {
|
|
896
|
+
if (options.module === "faillock") {
|
|
897
|
+
findings.push(...validateFaillockParams({
|
|
898
|
+
deny: typeof options.params.deny === "number" ? options.params.deny : undefined,
|
|
899
|
+
unlock_time: typeof options.params.unlock_time === "number" ? options.params.unlock_time : undefined,
|
|
900
|
+
fail_interval: typeof options.params.fail_interval === "number" ? options.params.fail_interval : undefined,
|
|
901
|
+
}));
|
|
902
|
+
}
|
|
903
|
+
else if (options.module === "pwquality") {
|
|
904
|
+
findings.push(...validatePwqualityParams({
|
|
905
|
+
minlen: typeof options.params.minlen === "number" ? options.params.minlen : undefined,
|
|
906
|
+
dcredit: typeof options.params.dcredit === "number" ? options.params.dcredit : undefined,
|
|
907
|
+
ucredit: typeof options.params.ucredit === "number" ? options.params.ucredit : undefined,
|
|
908
|
+
lcredit: typeof options.params.lcredit === "number" ? options.params.lcredit : undefined,
|
|
909
|
+
ocredit: typeof options.params.ocredit === "number" ? options.params.ocredit : undefined,
|
|
910
|
+
minclass: typeof options.params.minclass === "number" ? options.params.minclass : undefined,
|
|
911
|
+
maxrepeat: typeof options.params.maxrepeat === "number" ? options.params.maxrepeat : undefined,
|
|
912
|
+
retry: typeof options.params.retry === "number" ? options.params.retry : undefined,
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// 2. Config structure checks
|
|
917
|
+
if (options.lines) {
|
|
918
|
+
findings.push(...validatePamConfigSanity(options.lines));
|
|
919
|
+
}
|
|
920
|
+
// Sort: critical first, then warning; within same severity, by module
|
|
921
|
+
findings.sort((a, b) => {
|
|
922
|
+
if (a.severity !== b.severity) {
|
|
923
|
+
return a.severity === "critical" ? -1 : 1;
|
|
924
|
+
}
|
|
925
|
+
return a.module.localeCompare(b.module);
|
|
926
|
+
});
|
|
927
|
+
const criticalCount = findings.filter((f) => f.severity === "critical").length;
|
|
928
|
+
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
929
|
+
return {
|
|
930
|
+
safe: criticalCount === 0,
|
|
931
|
+
findings,
|
|
932
|
+
criticalCount,
|
|
933
|
+
warningCount,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/tools/access-control.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/tools/access-control.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA6PpE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0jElE"}
|
|
@@ -12,7 +12,40 @@ import { getConfig, getToolTimeout } from "../core/config.js";
|
|
|
12
12
|
import { getDistroAdapter } from "../core/distro-adapter.js";
|
|
13
13
|
import { createTextContent, createErrorContent, formatToolOutput, } from "../core/parsers.js";
|
|
14
14
|
import { logChange, createChangeEntry, backupFile, } from "../core/changelog.js";
|
|
15
|
-
import { parsePamConfig, serializePamConfig, validatePamConfig, createPamRule, removeModuleRules, insertBeforeModule, insertAfterModule, adjustJumpCounts, readPamFile, writePamFile, backupPamFile, restorePamFile, PamWriteError, } from "../core/pam-utils.js";
|
|
15
|
+
import { parsePamConfig, serializePamConfig, validatePamConfig, createPamRule, removeModuleRules, insertBeforeModule, insertAfterModule, adjustJumpCounts, readPamFile, writePamFile, backupPamFile, restorePamFile, PamWriteError, validatePamPolicySanity, } from "../core/pam-utils.js";
|
|
16
|
+
/**
|
|
17
|
+
* Downgrade a severity level by one step.
|
|
18
|
+
* critical → high, high → medium, medium → low, low → low
|
|
19
|
+
*/
|
|
20
|
+
function downgradeSeverity(severity) {
|
|
21
|
+
switch (severity) {
|
|
22
|
+
case "critical": return "high";
|
|
23
|
+
case "high": return "medium";
|
|
24
|
+
case "medium": return "low";
|
|
25
|
+
default: return severity;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Adjust finding severity and description based on SSH service state.
|
|
30
|
+
*/
|
|
31
|
+
function adjustFindingForServiceState(finding, serviceState) {
|
|
32
|
+
switch (serviceState) {
|
|
33
|
+
case "active":
|
|
34
|
+
return finding;
|
|
35
|
+
case "installed_inactive":
|
|
36
|
+
return {
|
|
37
|
+
severity: downgradeSeverity(finding.severity),
|
|
38
|
+
description: `[SERVICE STOPPED] ${finding.description}`,
|
|
39
|
+
};
|
|
40
|
+
case "removed_residual":
|
|
41
|
+
return {
|
|
42
|
+
severity: "info",
|
|
43
|
+
description: `[RESIDUAL CONFIG] ${finding.description}`,
|
|
44
|
+
};
|
|
45
|
+
case "not_installed":
|
|
46
|
+
return finding; // Should not reach here — not_installed short-circuits
|
|
47
|
+
}
|
|
48
|
+
}
|
|
16
49
|
const SSH_HARDENING_CHECKS = [
|
|
17
50
|
{
|
|
18
51
|
key: "PermitRootLogin",
|
|
@@ -285,6 +318,12 @@ export function registerAccessControlTools(server) {
|
|
|
285
318
|
.default("/usr/sbin/nologin")
|
|
286
319
|
.describe("Shell to set (restrict_shell, default: /usr/sbin/nologin)"),
|
|
287
320
|
// ── shared ──
|
|
321
|
+
force: z
|
|
322
|
+
.boolean()
|
|
323
|
+
.optional()
|
|
324
|
+
.default(false)
|
|
325
|
+
.describe("Force operation even if sanity checks report critical findings (pam_configure). " +
|
|
326
|
+
"Use with extreme caution — may cause lockouts."),
|
|
288
327
|
dry_run: z
|
|
289
328
|
.boolean()
|
|
290
329
|
.optional()
|
|
@@ -296,13 +335,90 @@ export function registerAccessControlTools(server) {
|
|
|
296
335
|
case "ssh_audit": {
|
|
297
336
|
try {
|
|
298
337
|
const config_path = params.config_path ?? "/etc/ssh/sshd_config";
|
|
338
|
+
// ── Step 1: Detect SSH service state ──────────────────────
|
|
339
|
+
const whichResult = await executeCommand({
|
|
340
|
+
command: "which",
|
|
341
|
+
args: ["sshd"],
|
|
342
|
+
toolName: "access_control",
|
|
343
|
+
timeout: 5000,
|
|
344
|
+
});
|
|
345
|
+
const sshdBinaryExists = whichResult.exitCode === 0;
|
|
346
|
+
const serviceResult = await executeCommand({
|
|
347
|
+
command: "systemctl",
|
|
348
|
+
args: ["is-active", "ssh", "sshd"],
|
|
349
|
+
toolName: "access_control",
|
|
350
|
+
timeout: 5000,
|
|
351
|
+
});
|
|
352
|
+
const serviceRunning = serviceResult.exitCode === 0;
|
|
353
|
+
// ── Step 2: Try to read the config file ──────────────────
|
|
299
354
|
const result = await executeCommand({
|
|
300
355
|
command: "sudo",
|
|
301
356
|
args: ["cat", config_path],
|
|
302
357
|
toolName: "access_control",
|
|
303
358
|
timeout: getToolTimeout("access_control"),
|
|
304
359
|
});
|
|
305
|
-
|
|
360
|
+
const configExists = result.exitCode === 0;
|
|
361
|
+
// ── Step 3: Classify service state ───────────────────────
|
|
362
|
+
let serviceStatus;
|
|
363
|
+
if (sshdBinaryExists && serviceRunning) {
|
|
364
|
+
serviceStatus = {
|
|
365
|
+
state: "active",
|
|
366
|
+
label: "ACTIVE (sshd running, accepting connections)",
|
|
367
|
+
sshdBinaryExists: true,
|
|
368
|
+
serviceRunning: true,
|
|
369
|
+
configExists,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
else if (sshdBinaryExists && !serviceRunning) {
|
|
373
|
+
serviceStatus = {
|
|
374
|
+
state: "installed_inactive",
|
|
375
|
+
label: "INSTALLED BUT INACTIVE (sshd binary found, service not running)",
|
|
376
|
+
sshdBinaryExists: true,
|
|
377
|
+
serviceRunning: false,
|
|
378
|
+
configExists,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
else if (!sshdBinaryExists && configExists) {
|
|
382
|
+
serviceStatus = {
|
|
383
|
+
state: "removed_residual",
|
|
384
|
+
label: "NOT INSTALLED (residual config file detected)",
|
|
385
|
+
sshdBinaryExists: false,
|
|
386
|
+
serviceRunning: false,
|
|
387
|
+
configExists: true,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
serviceStatus = {
|
|
392
|
+
state: "not_installed",
|
|
393
|
+
label: "NOT INSTALLED",
|
|
394
|
+
sshdBinaryExists: false,
|
|
395
|
+
serviceRunning: false,
|
|
396
|
+
configExists: false,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// ── Step 4: Short-circuit for not_installed ──────────────
|
|
400
|
+
if (serviceStatus.state === "not_installed") {
|
|
401
|
+
const entry = createChangeEntry({
|
|
402
|
+
tool: "access_control",
|
|
403
|
+
action: "SSH configuration audit",
|
|
404
|
+
target: config_path,
|
|
405
|
+
after: "SSH server not installed — audit skipped",
|
|
406
|
+
dryRun: false,
|
|
407
|
+
success: true,
|
|
408
|
+
});
|
|
409
|
+
logChange(entry);
|
|
410
|
+
const output = {
|
|
411
|
+
configPath: config_path,
|
|
412
|
+
serviceStatus: serviceStatus.label,
|
|
413
|
+
serviceState: serviceStatus.state,
|
|
414
|
+
summary: { passed: 0, failed: 0, warned: 0, total: 0 },
|
|
415
|
+
findings: [],
|
|
416
|
+
note: "SSH server is not installed on this system. No audit required.",
|
|
417
|
+
};
|
|
418
|
+
return { content: [formatToolOutput(output)] };
|
|
419
|
+
}
|
|
420
|
+
// ── Step 5: Config read failed but binary exists → error ─
|
|
421
|
+
if (!configExists) {
|
|
306
422
|
return {
|
|
307
423
|
content: [
|
|
308
424
|
createErrorContent(`Cannot read SSH config (exit ${result.exitCode}): ${result.stderr}`),
|
|
@@ -362,13 +478,15 @@ export function registerAccessControlTools(server) {
|
|
|
362
478
|
else {
|
|
363
479
|
status = currentValue.toLowerCase() === check.recommended.toLowerCase() ? "pass" : "fail";
|
|
364
480
|
}
|
|
481
|
+
// ── Step 6: Adjust severity based on service state ─────
|
|
482
|
+
const adjusted = adjustFindingForServiceState({ severity: check.severity, description: check.description }, serviceStatus.state);
|
|
365
483
|
findings.push({
|
|
366
484
|
setting: check.key,
|
|
367
485
|
currentValue,
|
|
368
486
|
recommendedValue: check.recommended,
|
|
369
487
|
status,
|
|
370
|
-
severity:
|
|
371
|
-
description:
|
|
488
|
+
severity: adjusted.severity,
|
|
489
|
+
description: adjusted.description,
|
|
372
490
|
});
|
|
373
491
|
}
|
|
374
492
|
const passed = findings.filter((f) => f.status === "pass").length;
|
|
@@ -378,16 +496,31 @@ export function registerAccessControlTools(server) {
|
|
|
378
496
|
tool: "access_control",
|
|
379
497
|
action: "SSH configuration audit",
|
|
380
498
|
target: config_path,
|
|
381
|
-
after: `Pass: ${passed}, Fail: ${failed}, Warn: ${warned}`,
|
|
499
|
+
after: `Pass: ${passed}, Fail: ${failed}, Warn: ${warned}, Service: ${serviceStatus.state}`,
|
|
382
500
|
dryRun: false,
|
|
383
501
|
success: true,
|
|
384
502
|
});
|
|
385
503
|
logChange(entry);
|
|
504
|
+
// ── Step 7: Build output with service status context ─────
|
|
386
505
|
const output = {
|
|
387
506
|
configPath: config_path,
|
|
507
|
+
serviceStatus: serviceStatus.label,
|
|
508
|
+
serviceState: serviceStatus.state,
|
|
388
509
|
summary: { passed, failed, warned, total: findings.length },
|
|
389
510
|
findings,
|
|
390
511
|
};
|
|
512
|
+
// Add contextual notes for non-active states
|
|
513
|
+
if (serviceStatus.state === "removed_residual") {
|
|
514
|
+
output.note =
|
|
515
|
+
"openssh-server has been removed but " + config_path + " remains. " +
|
|
516
|
+
"All findings below are INFORMATIONAL — no SSH server is running. " +
|
|
517
|
+
"Recommendation: sudo dpkg --purge openssh-server";
|
|
518
|
+
}
|
|
519
|
+
else if (serviceStatus.state === "installed_inactive") {
|
|
520
|
+
output.note =
|
|
521
|
+
"sshd is installed but the service is not currently running. " +
|
|
522
|
+
"Findings have been downgraded by one severity level.";
|
|
523
|
+
}
|
|
391
524
|
return { content: [formatToolOutput(output)] };
|
|
392
525
|
}
|
|
393
526
|
catch (err) {
|
|
@@ -954,6 +1087,27 @@ export function registerAccessControlTools(server) {
|
|
|
954
1087
|
maxrepeat: params.pam_settings?.maxrepeat ?? defaults.maxrepeat,
|
|
955
1088
|
reject_username: params.pam_settings?.reject_username ?? defaults.reject_username,
|
|
956
1089
|
};
|
|
1090
|
+
// ── Sanity check: detect dangerous pwquality parameters ──
|
|
1091
|
+
const pwqSanityResult = validatePamPolicySanity({
|
|
1092
|
+
module: "pwquality",
|
|
1093
|
+
params: merged,
|
|
1094
|
+
});
|
|
1095
|
+
if (pwqSanityResult.criticalCount > 0 && !(params.force)) {
|
|
1096
|
+
const findingsText = pwqSanityResult.findings
|
|
1097
|
+
.map(f => ` [${f.severity.toUpperCase()}] ${f.message}\n → ${f.recommendation}`)
|
|
1098
|
+
.join("\n");
|
|
1099
|
+
return {
|
|
1100
|
+
content: [
|
|
1101
|
+
createErrorContent(`PAM policy sanity check FAILED — ${pwqSanityResult.criticalCount} critical finding(s):\n\n` +
|
|
1102
|
+
findingsText +
|
|
1103
|
+
`\n\nTo override, set force=true. This may cause lockouts.`),
|
|
1104
|
+
],
|
|
1105
|
+
isError: true,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const pwqSanityWarnings = pwqSanityResult.findings
|
|
1109
|
+
.map(f => `[${f.severity.toUpperCase()}] ${f.message}`)
|
|
1110
|
+
.join("\n");
|
|
957
1111
|
const targetFile = "/etc/security/pwquality.conf";
|
|
958
1112
|
const configLines = [
|
|
959
1113
|
`minlen = ${merged.minlen}`,
|
|
@@ -978,7 +1132,8 @@ export function registerAccessControlTools(server) {
|
|
|
978
1132
|
return {
|
|
979
1133
|
content: [
|
|
980
1134
|
createTextContent(`[DRY-RUN] Would write the following to ${targetFile}:\n\n` +
|
|
981
|
-
configLines.map((l) => ` ${l}`).join("\n")
|
|
1135
|
+
configLines.map((l) => ` ${l}`).join("\n") +
|
|
1136
|
+
(pwqSanityWarnings ? `\n\n⚠️ Sanity warnings:\n${pwqSanityWarnings}` : "")),
|
|
982
1137
|
],
|
|
983
1138
|
};
|
|
984
1139
|
}
|
|
@@ -1023,7 +1178,8 @@ export function registerAccessControlTools(server) {
|
|
|
1023
1178
|
return {
|
|
1024
1179
|
content: [
|
|
1025
1180
|
createTextContent(`pam_pwquality configured in ${targetFile}:\n\n` +
|
|
1026
|
-
configLines.map((l) => ` ${l}`).join("\n")
|
|
1181
|
+
configLines.map((l) => ` ${l}`).join("\n") +
|
|
1182
|
+
(pwqSanityWarnings ? `\n\n⚠️ Sanity warnings:\n${pwqSanityWarnings}` : "")),
|
|
1027
1183
|
],
|
|
1028
1184
|
};
|
|
1029
1185
|
}
|
|
@@ -1054,6 +1210,27 @@ export function registerAccessControlTools(server) {
|
|
|
1054
1210
|
unlock_time: params.pam_settings?.unlock_time ?? defaults.unlock_time,
|
|
1055
1211
|
fail_interval: params.pam_settings?.fail_interval ?? defaults.fail_interval,
|
|
1056
1212
|
};
|
|
1213
|
+
// ── Sanity check: detect dangerous faillock parameters ──
|
|
1214
|
+
const flSanityResult = validatePamPolicySanity({
|
|
1215
|
+
module: "faillock",
|
|
1216
|
+
params: merged,
|
|
1217
|
+
});
|
|
1218
|
+
if (flSanityResult.criticalCount > 0 && !(params.force)) {
|
|
1219
|
+
const findingsText = flSanityResult.findings
|
|
1220
|
+
.map(f => ` [${f.severity.toUpperCase()}] ${f.message}\n → ${f.recommendation}`)
|
|
1221
|
+
.join("\n");
|
|
1222
|
+
return {
|
|
1223
|
+
content: [
|
|
1224
|
+
createErrorContent(`PAM policy sanity check FAILED — ${flSanityResult.criticalCount} critical finding(s):\n\n` +
|
|
1225
|
+
findingsText +
|
|
1226
|
+
`\n\nTo override, set force=true. This may cause lockouts.`),
|
|
1227
|
+
],
|
|
1228
|
+
isError: true,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const flSanityWarnings = flSanityResult.findings
|
|
1232
|
+
.map(f => `[${f.severity.toUpperCase()}] ${f.message}`)
|
|
1233
|
+
.join("\n");
|
|
1057
1234
|
const targetFile = (await getDistroAdapter()).paths.pamAuth;
|
|
1058
1235
|
const failArgsList = [
|
|
1059
1236
|
`deny=${merged.deny}`,
|
|
@@ -1077,7 +1254,8 @@ export function registerAccessControlTools(server) {
|
|
|
1077
1254
|
content: [
|
|
1078
1255
|
createTextContent(`[DRY-RUN] Would add/update pam_faillock.so in ${targetFile}:\n\n` +
|
|
1079
1256
|
` ${preLine}\n ${authLine}\n\n` +
|
|
1080
|
-
`Settings: ${JSON.stringify(merged)}`
|
|
1257
|
+
`Settings: ${JSON.stringify(merged)}` +
|
|
1258
|
+
(flSanityWarnings ? `\n\n⚠️ Sanity warnings:\n${flSanityWarnings}` : "")),
|
|
1081
1259
|
],
|
|
1082
1260
|
};
|
|
1083
1261
|
}
|
|
@@ -1110,6 +1288,11 @@ export function registerAccessControlTools(server) {
|
|
|
1110
1288
|
if (!recheckValidation.valid) {
|
|
1111
1289
|
throw new PamWriteError(`Generated PAM config failed validation: ${recheckValidation.errors.join("; ")}`, targetFile, backupEntry.id);
|
|
1112
1290
|
}
|
|
1291
|
+
// 7b. Config-level sanity check on the resulting PAM structure
|
|
1292
|
+
const configSanity = validatePamPolicySanity({ lines: recheckLines });
|
|
1293
|
+
if (configSanity.criticalCount > 0 && !(params.force)) {
|
|
1294
|
+
throw new PamWriteError(`PAM config sanity check failed: ${configSanity.findings.map(f => f.message).join("; ")}`, targetFile, backupEntry.id);
|
|
1295
|
+
}
|
|
1113
1296
|
// 8. Write (validates pre and post-write internally)
|
|
1114
1297
|
await writePamFile(targetFile, newContent);
|
|
1115
1298
|
const entry = createChangeEntry({
|
|
@@ -1127,7 +1310,8 @@ export function registerAccessControlTools(server) {
|
|
|
1127
1310
|
createTextContent(`pam_faillock configured in ${targetFile}:\n\n` +
|
|
1128
1311
|
` ${preLine}\n ${authLine}\n\n` +
|
|
1129
1312
|
`Settings: ${JSON.stringify(merged)}\n` +
|
|
1130
|
-
`Backup: ${backupEntry.backupPath}`
|
|
1313
|
+
`Backup: ${backupEntry.backupPath}` +
|
|
1314
|
+
(flSanityWarnings ? `\n\n⚠️ Sanity warnings:\n${flSanityWarnings}` : "")),
|
|
1131
1315
|
],
|
|
1132
1316
|
};
|
|
1133
1317
|
}
|
package/package.json
CHANGED