mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-19 13:17:23 +00:00
Focus the Audit & Report tab, scope API tokens
This commit is contained in:
@@ -111,7 +111,7 @@ def registered_checks() -> list[Check]:
|
||||
class AuditContext:
|
||||
"""Lazily collects each source once and shares it across checks."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, run_lynis: bool = False):
|
||||
self._cache: dict[str, Any] = {}
|
||||
self._source_info = {}
|
||||
self._dependencies = {}
|
||||
@@ -119,6 +119,10 @@ class AuditContext:
|
||||
self._errors = {}
|
||||
self._check_deadline = float("inf")
|
||||
self._run_deadline = time.monotonic() + RUN_TIMEOUT
|
||||
# Whether this assessment may launch Lynis. The user grants it in
|
||||
# the run dialog; without it the audit reads a stored report and
|
||||
# never starts one, so a run is fast and predictable.
|
||||
self._run_lynis_allowed = run_lynis
|
||||
|
||||
def begin_check(self, budget: int = CHECK_TIMEOUT):
|
||||
self._sources_used = set()
|
||||
@@ -332,14 +336,39 @@ class AuditContext:
|
||||
user launched from that page is waited on rather than duplicated.
|
||||
"""
|
||||
def load():
|
||||
from security_manager import parse_lynis_report
|
||||
from security_manager import parse_lynis_report, _find_lynis_cmd
|
||||
parsed = parse_lynis_report(enrich_current=False)
|
||||
ran, run_error = False, None
|
||||
if parsed is None or not parsed.get("is_complete"):
|
||||
produced, ran, run_error = self._run_lynis()
|
||||
if produced is not None:
|
||||
parsed = produced
|
||||
# Launch Lynis only when the assessment was granted permission
|
||||
# (the user chose "with Lynis"). Then run it if there is no
|
||||
# usable report, or refresh a stored one that is past the
|
||||
# staleness threshold, since running is precisely what the
|
||||
# user consented to.
|
||||
if self._run_lynis_allowed:
|
||||
need_run = parsed is None or not parsed.get("is_complete")
|
||||
if not need_run:
|
||||
src = next((p for p in (Path("/var/log/lynis-report.dat"),
|
||||
Path("/var/log/lynis-output.log"))
|
||||
if p.exists()), None)
|
||||
if src:
|
||||
age_days = (time.time() - src.stat().st_mtime) / 86400
|
||||
need_run = age_days >= self.policy.threshold("lynis_report_days")
|
||||
if need_run:
|
||||
produced, ran, run_error = self._run_lynis()
|
||||
if produced is not None:
|
||||
parsed = produced
|
||||
if parsed is None:
|
||||
# No stored report and none produced. Where Lynis is
|
||||
# installed the check should say it was not run rather than
|
||||
# that it does not apply, so the reader knows a reading is
|
||||
# available on request.
|
||||
if not self._run_lynis_allowed and _find_lynis_cmd():
|
||||
return {"mtime": 0, "source": "", "version": None,
|
||||
"warnings": [], "suggestions": [],
|
||||
"hardening_index": None, "complete": False,
|
||||
"produced_here": False,
|
||||
"run_error": "Lynis is installed but was not run "
|
||||
"for this assessment."}
|
||||
return None
|
||||
source = next((p for p in (Path("/var/log/lynis-report.dat"),
|
||||
Path("/var/log/lynis-output.log")) if p.exists()), None)
|
||||
@@ -534,7 +563,8 @@ def _classification_of(result: dict, check: "Check") -> str:
|
||||
|
||||
|
||||
def run_assessment(profile: str = "full",
|
||||
only_areas: Optional[set[str]] = None, *, run_id=None, progress=None) -> str:
|
||||
only_areas: Optional[set[str]] = None, *, run_id=None,
|
||||
progress=None, run_lynis: bool = False) -> str:
|
||||
"""Evaluate every registered check and persist the result.
|
||||
|
||||
A check that raises is recorded as unverified with the error kept
|
||||
@@ -551,7 +581,7 @@ def run_assessment(profile: str = "full",
|
||||
checks = audit_profiles.selected_checks(profile, registered_checks())
|
||||
if only_areas is not None:
|
||||
checks = [c for c in checks if c.area in only_areas]
|
||||
ctx = AuditContext()
|
||||
ctx = AuditContext(run_lynis=run_lynis)
|
||||
exceptions = audit_store.active_exceptions()
|
||||
metadata = ctx.metadata(checks)
|
||||
if run_id is None:
|
||||
|
||||
@@ -3080,7 +3080,10 @@ def _host_recovery(ctx):
|
||||
"summary_key": "scheduledOnly",
|
||||
"summary_params": {"count": str(len(scheduled))},
|
||||
"evidence": evidence}
|
||||
return {"classification": CLASS_WARNING, "summary_key": "noHostBackup",
|
||||
# A host-configuration backup is a ProxMenux feature the operator
|
||||
# may simply not have set up; its absence is not a fault of the
|
||||
# host. Reported as an observation, not a warning.
|
||||
return {"classification": CLASS_OBSERVATION, "summary_key": "noHostBackup",
|
||||
"evidence": evidence}
|
||||
|
||||
affected = []
|
||||
|
||||
@@ -578,6 +578,10 @@ def list_api_tokens():
|
||||
entry = {
|
||||
"id": t.get("id"),
|
||||
"name": t.get("name", "API Token"),
|
||||
# Tokens issued before scope existed carry no claim and verify
|
||||
# as full_admin, so the list reflects that rather than the
|
||||
# read-only default a new token gets.
|
||||
"scope": t.get("scope", "full_admin"),
|
||||
"token_prefix": t.get("token_prefix", "***"),
|
||||
"created_at": t.get("created_at"),
|
||||
"expires_at": t.get("expires_at"),
|
||||
|
||||
@@ -145,6 +145,10 @@ def run():
|
||||
any(not isinstance(a, str) or a not in audit_checks.AREAS for a in areas)))):
|
||||
return jsonify(success=False, message="Unsupported audit profile or areas"), 400
|
||||
only = set(areas) if areas is not None else None
|
||||
# The caller consents to Lynis running as part of the assessment; the
|
||||
# interface asks the user before setting it, since a run can take a few
|
||||
# minutes. Absent or false, the audit reads any stored report instead.
|
||||
run_lynis = bool(data.get('run_lynis'))
|
||||
|
||||
with _run_lock:
|
||||
if _running['active']:
|
||||
@@ -159,7 +163,8 @@ def run():
|
||||
|
||||
def worker():
|
||||
try:
|
||||
audit_checks.run_assessment(profile, only_areas=only, run_id=run_id, progress=_progress)
|
||||
audit_checks.run_assessment(profile, only_areas=only, run_id=run_id,
|
||||
progress=_progress, run_lynis=run_lynis)
|
||||
audit_store.prune_runs()
|
||||
except Exception as e:
|
||||
audit_store.finish_run(run_id, checks_total=_running.get('completed', 0), error=str(e))
|
||||
@@ -378,6 +383,39 @@ def profiles():
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@audit_bp.route('/api/audit/lynis-readiness', methods=['GET'])
|
||||
@require_auth
|
||||
def lynis_readiness():
|
||||
"""Whether a security assessment would need to run Lynis.
|
||||
|
||||
The interface reads this before starting a run whose profile includes
|
||||
the Lynis check, so it can ask the user whether to run the audit
|
||||
(which takes a few minutes) or reuse a stored report. Touches no host
|
||||
state beyond reading the existing report file's age.
|
||||
"""
|
||||
try:
|
||||
import security_manager
|
||||
from pathlib import Path
|
||||
installed = bool(security_manager._find_lynis_cmd())
|
||||
parsed = (security_manager.parse_lynis_report(enrich_current=False)
|
||||
if installed else None)
|
||||
complete = bool(parsed and parsed.get("is_complete"))
|
||||
age_days = None
|
||||
if complete:
|
||||
src = next((p for p in (Path("/var/log/lynis-report.dat"),
|
||||
Path("/var/log/lynis-output.log"))
|
||||
if p.exists()), None)
|
||||
if src:
|
||||
age_days = round((time.time() - src.stat().st_mtime) / 86400, 1)
|
||||
limit = audit_policy.load().threshold("lynis_report_days") if audit_policy else 30
|
||||
return jsonify({"success": True, "installed": installed,
|
||||
"has_report": complete, "age_days": age_days,
|
||||
"stale_days": limit,
|
||||
"stale": age_days is not None and age_days >= limit})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@audit_bp.route('/api/audit/policy', methods=['GET'])
|
||||
@require_auth
|
||||
def policy():
|
||||
|
||||
Reference in New Issue
Block a user