Skip to content

Commit e23fe78

Browse files
committed
release: 0.9.8.1 — audit-grade compliance PDFs, hardening fixes, log rotation
Compliance Dashboard - Per-framework PDF restructured to a real audit-report layout (cover, disclaimer up front, executive summary with posture rating, top findings, scope of assessment, methodology, findings by severity, per-node coverage, per-family detail with severity column, appendix B remediation plan with priority and recommended timeline, appendix C evidence with verbose check output, appendix D glossary). - Real CMMC L1/L2 (NIST 800-171), NIST 800-53 Mod, DISA STIG, ISO 27001:2022 Annex A, BSI Grundschutz, VS-NfD control IDs mapped to PegaProx internal checks (47 controls × 7 frameworks). Lives in core/compliance_mapping.py. - Per-control severity (high/medium/low/informational) and remediation timeline (within 30 / 90 / 180 days). - "PegaProx control" column in the per-family / remediation tables matches the checkbox names in Settings → Compliance → Harden PVE Node, with an explicit operator-handoff note. - New API endpoint GET /api/compliance/mapping serves the data structure. Hardening - pw_quality now wires pam_pwquality.so into /etc/pam.d/common-password. - pw_history avoids use_authtok unless pwquality is configured ahead of it, preventing "Authentication token manipulation error" on every passwd call. - New control: pam_password_repair (Repair PAM password stack — recovery) detects + fixes the broken stack in one click. Idempotent on healthy systems. Logging - Per-cluster operational log capped at 3h via new utils/log_handler.CappedTimedFileHandler (#345, #348). Audit log unaffected. - SSH error log now shows the real stderr line instead of the SSH banner-padding asterisks. Bugfixes - Modern Layout Re-configure Cluster icon now works — setReconfigureCluster prop was missing in ClusterSidebarItem (#346). - Manifest carry-over from #344: 4 new 0.9.8 modules in update_files. Sponsors - New Silver Sponsor: uvensys GmbH.
1 parent 5b76837 commit e23fe78

12 files changed

Lines changed: 2227 additions & 121 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</p>
1717

1818
<p align="center">
19-
<img src="https://img.shields.io/badge/version-0.9.8--beta-blue" alt="Version"/>
19+
<img src="https://img.shields.io/badge/version-0.9.8.1--beta-blue" alt="Version"/>
2020
<img src="https://img.shields.io/badge/python-3.8+-green" alt="Python"/>
2121
<img src="https://img.shields.io/badge/license-AGPL--3.0--License-orange" alt="License"/>
2222
</p>
@@ -51,7 +51,9 @@ PegaProx is a powerful web-based management interface for Proxmox VE and XCP-ng
5151
### 🥈 Silver
5252

5353
<p align="center">
54-
<em>Your logo here</em> — <a href="mailto:sponsor@pegaprox.com">Become a Silver Sponsor</a>
54+
<a href="https://uvensys.de/">
55+
<img src="images/sponsors/uvensys.png" alt="uvensys GmbH" width="160"/>
56+
</a>
5557
</p>
5658

5759
### 🥉 Bronze

images/sponsors/uvensys.png

148 KB
Loading

pegaprox/api/reports.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,49 @@ def apply_hardening(cluster_id, node):
646646
})
647647

648648

649+
# ============================================
650+
# Compliance framework mapping - MK Apr 2026
651+
# Frontend pulls these to render the compliance PDFs with real
652+
# CMMC / NIST / STIG / ISO / BSI control IDs instead of our internal
653+
# names like "pam_faillock". See pegaprox/core/compliance_mapping.py
654+
# ============================================
655+
656+
@bp.route('/api/compliance/mapping', methods=['GET'])
657+
@require_auth()
658+
def compliance_mapping_api():
659+
framework = (request.args.get('framework', '') or '').strip().lower()
660+
from pegaprox.core import compliance_mapping as cm
661+
payload_full = {
662+
'family_labels': cm.FAMILY_LABELS,
663+
'mappings': cm.FRAMEWORK_MAPPING,
664+
'remediation': cm.REMEDIATION,
665+
'severity': cm.SEVERITY,
666+
'recommended_timeline': cm.RECOMMENDED_TIMELINE,
667+
'priority_level': cm.PRIORITY_LEVEL,
668+
'framework_meta': cm.FRAMEWORK_META,
669+
'posture_levels': cm.POSTURE_LEVELS,
670+
'glossary': cm.GLOSSARY,
671+
'methodology': cm.METHODOLOGY,
672+
}
673+
if not framework:
674+
return jsonify(payload_full)
675+
if framework not in cm.FRAMEWORK_MAPPING:
676+
return jsonify({'error': f'unknown framework: {framework}'}), 400
677+
return jsonify({
678+
'framework': framework,
679+
'family_labels': cm.FAMILY_LABELS,
680+
'mapping': cm.FRAMEWORK_MAPPING[framework],
681+
'remediation': cm.REMEDIATION,
682+
'severity': cm.SEVERITY,
683+
'recommended_timeline': cm.RECOMMENDED_TIMELINE,
684+
'priority_level': cm.PRIORITY_LEVEL,
685+
'framework_meta': cm.FRAMEWORK_META.get(framework, {}),
686+
'posture_levels': cm.POSTURE_LEVELS,
687+
'glossary': cm.GLOSSARY,
688+
'methodology': cm.METHODOLOGY,
689+
})
690+
691+
649692
# ============================================
650693
# Legacy Fallback Endpoints
651694
# old tags endpoints, kept for compat, these prevent 404s

pegaprox/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from pathlib import Path
99

1010
# Version
11-
PEGAPROX_VERSION = "Beta 0.9.8"
12-
PEGAPROX_BUILD = "2026.04.25"
11+
PEGAPROX_VERSION = "Beta 0.9.8.1"
12+
PEGAPROX_BUILD = "2026.04.28"
1313

1414
# File Paths & Directories
1515
CONFIG_DIR = 'config'

pegaprox/core/compliance_mapping.py

Lines changed: 1265 additions & 0 deletions
Large diffs are not rendered by default.

pegaprox/core/manager.py

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@ def _wrap_with_sudo(cmd):
156156
return f"echo {enc} | base64 -d | sudo -n bash"
157157

158158

159+
def _ssh_stderr_excerpt(stderr, max_chars=240):
160+
"""Last meaningful line of SSH stderr, capped.
161+
162+
OpenSSH dumps the full pre-auth banner (often a 70+ char asterisk border
163+
followed by AUP text) BEFORE the actual error like
164+
'Permission denied (publickey,password)' / 'Connection refused' /
165+
'Host key verification failed'. Logging stderr[:200] then shows nothing
166+
but banner. Take the last non-empty line — that's where SSH puts the
167+
error. MK Apr 2026.
168+
"""
169+
if not stderr:
170+
return 'no error output'
171+
lines = [ln.strip() for ln in stderr.splitlines() if ln.strip()]
172+
if not lines:
173+
return 'no error output'
174+
return lines[-1][:max_chars]
175+
176+
159177
class PegaProxManager:
160178
"""
161179
main cluster manager - NS
@@ -309,11 +327,11 @@ def __init__(self, cluster_id: str, config: PegaProxConfig):
309327
if self.logger.handlers:
310328
self.logger.handlers.clear()
311329

312-
# File handler - DEBUG level (for troubleshooting). Capped at 6h of data,
313-
# rotated content is discarded — see #345 / #348 (#348: shipped VM was
314-
# filling its own disk on busy clusters).
330+
# File handler - DEBUG level (for troubleshooting). Capped at 3h of data,
331+
# rotated content is discarded — see #345 / #348. 3h because 20+ node
332+
# clusters can hit ~100 MB/h, 6h was still rough on small disks.
315333
from pegaprox.utils.log_handler import CappedTimedFileHandler
316-
fh = CappedTimedFileHandler(f"{LOG_DIR}/{cluster_id}.log", when='H', interval=6, backupCount=0)
334+
fh = CappedTimedFileHandler(f"{LOG_DIR}/{cluster_id}.log", when='H', interval=3, backupCount=0)
317335
fh.setLevel(logging.DEBUG)
318336

319337
# Console handler - INFO level (no DEBUG spam)
@@ -5013,7 +5031,7 @@ def _ssh_run_command_output(self, host: str, user: str, command: str, timeout: i
50135031
)
50145032
if result.returncode == 0:
50155033
return result.stdout
5016-
self.logger.debug(f"[SSH] Command failed on {host}: {result.stderr[:200] if result.stderr else 'no error output'}")
5034+
self.logger.debug(f"[SSH] Command failed on {host}: {_ssh_stderr_excerpt(result.stderr)}")
50175035
return None
50185036
except subprocess.TimeoutExpired:
50195037
self.logger.debug(f"[SSH] Command timed out on {host}")
@@ -5054,7 +5072,7 @@ def _ssh_run_command_with_key_output(self, host: str, user: str, command: str, k
50545072
)
50555073
if result.returncode == 0:
50565074
return result.stdout
5057-
self.logger.debug(f"[SSH] Key auth failed on {host}: {result.stderr[:200] if result.stderr else 'no error output'}")
5075+
self.logger.debug(f"[SSH] Key auth failed on {host}: {_ssh_stderr_excerpt(result.stderr)}")
50585076
return None
50595077
finally:
50605078
os.unlink(key_file)
@@ -5092,7 +5110,7 @@ def _ssh_run_command_with_password_output(self, host: str, user: str, command: s
50925110
)
50935111
if result.returncode == 0:
50945112
return result.stdout
5095-
self.logger.debug(f"[SSH] Password auth failed on {host}: {result.stderr[:200] if result.stderr else 'no error output'}")
5113+
self.logger.debug(f"[SSH] Password auth failed on {host}: {_ssh_stderr_excerpt(result.stderr)}")
50965114
return None
50975115
except FileNotFoundError:
50985116
self.logger.debug(f"[SSH] sshpass not installed - cannot use password auth")
@@ -12623,16 +12641,24 @@ def scan_node_packages(self, node_name):
1262312641
},
1262412642
'pw_history': {
1262512643
'check': """grep -q 'pam_pwhistory.so' /etc/pam.d/common-password 2>/dev/null && echo OK || echo FAIL""",
12644+
# MK Apr 2026 — bugfix: do NOT use_authtok unless pam_pwquality.so is already
12645+
# in the stack ahead of us. Without a prior module to set the authtok the
12646+
# `passwd` command dies with "Authentication token manipulation error" because
12647+
# pwhistory is asked to use a token that nobody set. Reported by NS while
12648+
# testing CIS hardening on PVE 9.
1262612649
'apply': """if ! grep -q pam_pwhistory /etc/pam.d/common-password 2>/dev/null; then
1262712650
if grep -q 'pam_unix.so' /etc/pam.d/common-password 2>/dev/null; then
1262812651
cp /etc/pam.d/common-password /etc/pam.d/common-password.bak.cis
12629-
sed -i '/pam_unix.so/i password required pam_pwhistory.so remember=24 use_authtok' /etc/pam.d/common-password
12630-
# verify PAM still valid, rollback if broken
12631-
if ! pam_tally2 --help >/dev/null 2>&1 && ! pamtester --help >/dev/null 2>&1; then
12632-
# no PAM test tool available, at least verify file not empty
12633-
if [ ! -s /etc/pam.d/common-password ]; then
12634-
cp /etc/pam.d/common-password.bak.cis /etc/pam.d/common-password
12635-
fi
12652+
if grep -q 'pam_pwquality.so' /etc/pam.d/common-password 2>/dev/null; then
12653+
PWHIST_LINE='password required pam_pwhistory.so remember=24 use_authtok'
12654+
else
12655+
# No pwquality yet — let pwhistory prompt itself instead of expecting a chained token.
12656+
PWHIST_LINE='password required pam_pwhistory.so remember=24'
12657+
fi
12658+
sed -i "/pam_unix.so/i $PWHIST_LINE" /etc/pam.d/common-password
12659+
# rollback if file ended up empty (sed misbehavior on weird locales etc.)
12660+
if [ ! -s /etc/pam.d/common-password ]; then
12661+
cp /etc/pam.d/common-password.bak.cis /etc/pam.d/common-password
1263612662
fi
1263712663
fi
1263812664
fi
@@ -12698,7 +12724,12 @@ def scan_node_packages(self, node_name):
1269812724
echo DONE""",
1269912725
},
1270012726
'pw_quality': {
12701-
'check': """dpkg -l libpam-pwquality 2>/dev/null | grep -q '^ii' && echo OK || echo FAIL""",
12727+
# MK Apr 2026 — also add pam_pwquality.so to the PAM stack. Previous version
12728+
# only installed the package + wrote pwquality.conf which did nothing because
12729+
# the module was never wired into common-password. Insert BEFORE pam_pwhistory
12730+
# if it's already there (so pwquality runs first → sets authtok → pwhistory
12731+
# uses_authtok), else before pam_unix.
12732+
'check': """dpkg -l libpam-pwquality 2>/dev/null | grep -q '^ii' && grep -q 'pam_pwquality.so' /etc/pam.d/common-password 2>/dev/null && echo OK || echo FAIL""",
1270212733
'apply': """apt-get install -y libpam-pwquality >/dev/null 2>&1
1270312734
cat > /etc/security/pwquality.conf << 'PWEOF'
1270412735
# Lynis AUTH-9262: Password quality requirements
@@ -12712,6 +12743,90 @@ def scan_node_packages(self, node_name):
1271212743
gecoscheck = 1
1271312744
dictcheck = 1
1271412745
PWEOF
12746+
if ! grep -q 'pam_pwquality.so' /etc/pam.d/common-password 2>/dev/null; then
12747+
if grep -q 'pam_unix.so' /etc/pam.d/common-password 2>/dev/null; then
12748+
cp /etc/pam.d/common-password /etc/pam.d/common-password.bak.cis-pwq
12749+
if grep -q 'pam_pwhistory.so' /etc/pam.d/common-password 2>/dev/null; then
12750+
sed -i '/pam_pwhistory.so/i password requisite pam_pwquality.so retry=3' /etc/pam.d/common-password
12751+
else
12752+
sed -i '/pam_unix.so/i password requisite pam_pwquality.so retry=3' /etc/pam.d/common-password
12753+
fi
12754+
if [ ! -s /etc/pam.d/common-password ]; then
12755+
cp /etc/pam.d/common-password.bak.cis-pwq /etc/pam.d/common-password
12756+
fi
12757+
fi
12758+
fi
12759+
echo DONE""",
12760+
},
12761+
# MK Apr 2026 — Recovery control for PAM password stack. Detects the
12762+
# `pam_pwhistory.so use_authtok` without prior `pam_pwquality.so` situation
12763+
# (causes "Authentication token manipulation error" on every passwd call),
12764+
# repairs by either inserting pam_pwquality before pwhistory or stripping
12765+
# use_authtok if libpam-pwquality isn't installed. Idempotent — re-running
12766+
# on a healthy system is a no-op.
12767+
'pam_password_repair': {
12768+
# MK Apr 2026 — IMPORTANT: this check runs as part of a composite
12769+
# `{ check1; }; { check2; }; ...` chain. Brace groups share the parent
12770+
# shell's process, so a stray `exit 0` here would terminate ALL
12771+
# subsequent control checks. Use elif chain instead — no exits.
12772+
'check': """COMMON=/etc/pam.d/common-password
12773+
if [ ! -f "$COMMON" ]; then
12774+
echo OK
12775+
elif ! grep -q 'pam_pwhistory.so' "$COMMON" 2>/dev/null; then
12776+
echo OK
12777+
elif ! grep 'pam_pwhistory.so' "$COMMON" | grep -q 'use_authtok'; then
12778+
echo OK
12779+
elif awk '/pam_pwquality.so/{q=NR} /pam_pwhistory.so/{h=NR} END{exit !(q && q<h)}' "$COMMON"; then
12780+
echo OK
12781+
else
12782+
echo FAIL
12783+
fi""",
12784+
'verbose_check': """COMMON=/etc/pam.d/common-password
12785+
echo '--- /etc/pam.d/common-password (password-stack lines) ---'
12786+
grep -nE 'password\\s+(required|requisite|sufficient|optional)' "$COMMON" 2>/dev/null || echo '(no password lines found)'
12787+
echo '--- libpam-pwquality status ---'
12788+
dpkg -l libpam-pwquality 2>/dev/null | tail -1 || echo '(not installed)'""",
12789+
'apply': """COMMON=/etc/pam.d/common-password
12790+
if [ ! -f "$COMMON" ]; then
12791+
echo "(no $COMMON — nothing to repair)"
12792+
echo DONE; exit 0
12793+
fi
12794+
if ! grep -q 'pam_pwhistory.so' "$COMMON" 2>/dev/null; then
12795+
echo "(pam_pwhistory not configured — nothing to repair)"
12796+
echo DONE; exit 0
12797+
fi
12798+
# already healthy?
12799+
if awk '/pam_pwquality.so/{q=NR} /pam_pwhistory.so/{h=NR} END{exit !(q && q<h)}' "$COMMON"; then
12800+
if grep 'pam_pwhistory.so' "$COMMON" | grep -q 'use_authtok'; then
12801+
echo "(pam stack healthy: pwquality > pwhistory)"
12802+
echo DONE; exit 0
12803+
fi
12804+
fi
12805+
# also healthy if pwhistory has no use_authtok (it prompts itself, no chain needed)
12806+
if ! grep 'pam_pwhistory.so' "$COMMON" | grep -q 'use_authtok'; then
12807+
echo "(pam stack healthy: pwhistory standalone, no use_authtok)"
12808+
echo DONE; exit 0
12809+
fi
12810+
TS=$(date +%s)
12811+
cp "$COMMON" "$COMMON.bak.repair-$TS"
12812+
# Prefer wiring in pwquality if the package is installed, else strip use_authtok.
12813+
if dpkg -l libpam-pwquality 2>/dev/null | grep -q '^ii'; then
12814+
# remove any malformed pwquality line first
12815+
sed -i '/pam_pwquality.so/d' "$COMMON"
12816+
# insert pwquality requisite line right BEFORE the pwhistory line
12817+
sed -i '/pam_pwhistory.so/i password requisite pam_pwquality.so retry=3' "$COMMON"
12818+
echo "added pam_pwquality.so before pam_pwhistory.so (backup: $COMMON.bak.repair-$TS)"
12819+
else
12820+
# no pwquality package — strip use_authtok so pwhistory prompts itself
12821+
sed -i 's| use_authtok||g' "$COMMON"
12822+
echo "removed use_authtok from pam_pwhistory.so (libpam-pwquality not installed) (backup: $COMMON.bak.repair-$TS)"
12823+
fi
12824+
# safety: empty file? roll back
12825+
if [ ! -s "$COMMON" ]; then
12826+
cp "$COMMON.bak.repair-$TS" "$COMMON"
12827+
echo "ROLLED BACK — file was emptied by sed"
12828+
exit 1
12829+
fi
1271512830
echo DONE""",
1271612831
},
1271712832
'pw_aging': {

pegaprox/core/xcpng.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ def __init__(self, cluster_id: str, config):
135135
self.logger.propagate = False
136136
if self.logger.handlers:
137137
self.logger.handlers.clear()
138-
# 6h cap on the per-cluster log to keep disk under control (#345/#348)
138+
# 3h cap on the per-cluster log to keep disk under control (#345/#348)
139139
from pegaprox.utils.log_handler import CappedTimedFileHandler
140-
fh = CappedTimedFileHandler(f"{LOG_DIR}/{cluster_id}.log", when='H', interval=6, backupCount=0)
140+
fh = CappedTimedFileHandler(f"{LOG_DIR}/{cluster_id}.log", when='H', interval=3, backupCount=0)
141141
fh.setLevel(logging.DEBUG)
142142
ch = logging.StreamHandler()
143143
ch.setLevel(logging.INFO)

version.json

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
{
2-
"version": "0.9.8",
3-
"build": "2026.04.25",
4-
"release_date": "2026-04-25",
2+
"version": "0.9.8.1",
3+
"build": "2026.04.28",
4+
"release_date": "2026-04-28",
55
"changelog": [
6+
"Compliance Dashboard: per-framework PDFs restructured to a real audit-report layout (cover, executive summary, scope, methodology, findings by severity, per-family detail with severity column, remediation appendix with priority + recommended timeline, evidence appendix with verbose check output, glossary)",
7+
"Compliance: real CMMC L1/L2 (NIST 800-171), NIST 800-53, DISA STIG, ISO 27001 Annex A 2022, BSI Grundschutz, VS-NfD control IDs mapped to PegaProx internal checks (47 controls × 7 frameworks)",
8+
"Compliance: per-control severity rating (high/medium/low/informational) and recommended remediation timeline (within 30/90/180 days)",
9+
"Hardening: pw_quality now wires pam_pwquality.so into the PAM password stack (previous version only installed the package + wrote pwquality.conf)",
10+
"Hardening: pw_history avoids use_authtok unless pam_pwquality.so is configured ahead of it — fixes 'Authentication token manipulation error' on every passwd call",
11+
"Hardening: new recovery control 'Repair PAM password stack' (pam_password_repair) — one-click fix for systems with the broken PAM stack",
12+
"Per-cluster operational log capped at 3h of writes — bounded disk use even on 20+-node clusters (#345, #348). Audit log unaffected",
13+
"SSH error logging now shows the actual error instead of the SSH banner asterisks",
14+
"Modern Layout: Re-configure Cluster icon now works (setReconfigureCluster prop was missing in the sidebar component) (#346)",
15+
"Manifest fix: 4 new 0.9.8 modules (webauthn, metrics_exporter, ssh_pool, webhooks) added to update_files for incremental updaters (#344)",
16+
"New Silver Sponsor: uvensys GmbH — thanks!",
617
"ESXi → Proxmox migration: near-zero-downtime live mirror, multi-disk Windows support, 4K LUN compatibility",
718
"Optional VirtIO driver pre-staging for migrated Windows guests (works on LVM, ZFS, Ceph RBD and NFS targets)",
819
"Better ESXi connection reliability — periodic keepalive + auto-reconnect on stale sessions",
@@ -32,6 +43,7 @@
3243
"images/sponsors/sponsor1.png",
3344
"images/sponsors/sponsor2.png",
3445
"images/sponsors/idkmanager.png",
46+
"images/sponsors/uvensys.png",
3547
"misc/proxmox-lxc-appliance-creator.sh",
3648
"pegaprox/__init__.py",
3749
"pegaprox/api/__init__.py",
@@ -75,6 +87,7 @@
7587
"pegaprox/constants.py",
7688
"pegaprox/core/__init__.py",
7789
"pegaprox/core/cache.py",
90+
"pegaprox/core/compliance_mapping.py",
7891
"pegaprox/core/config.py",
7992
"pegaprox/core/db.py",
8093
"pegaprox/core/manager.py",

0 commit comments

Comments
 (0)