From 0b64549230a773a8f660fab54a899cf80fc1f972 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 12 Sep 2026 00:22:14 +0200 Subject: [PATCH] Focus the Audit & Report tab, scope API tokens --- AppImage/components/audit-comparison.tsx | 16 +-- AppImage/components/audit-report.tsx | 95 +++++++++++------ AppImage/components/latency-detail-modal.tsx | 11 +- AppImage/components/release-notes-modal.tsx | 40 +++---- AppImage/components/security.tsx | 65 +++++++++++- AppImage/lib/audit-document.ts | 103 ++++++++++++++++++- AppImage/lib/report-diagrams.ts | 31 +++--- AppImage/lib/report-shell.ts | 2 + AppImage/lib/version.ts | 2 +- AppImage/messages/de/common.json | 38 ++++++- AppImage/messages/en/common.json | 38 ++++++- AppImage/messages/es/common.json | 56 +++++++--- AppImage/messages/fr/common.json | 38 ++++++- AppImage/messages/it/common.json | 38 ++++++- AppImage/messages/pt/common.json | 38 ++++++- AppImage/messages/sk/common.json | 38 ++++++- AppImage/messages/sv/common.json | 38 ++++++- AppImage/package-lock.json | 4 +- AppImage/package.json | 2 +- AppImage/scripts/audit_checks.py | 46 +++++++-- AppImage/scripts/audit_checks_pve.py | 5 +- AppImage/scripts/auth_manager.py | 4 + AppImage/scripts/flask_audit_routes.py | 40 ++++++- json/app_tracking_hints.json | 15 +++ json/runtime_verified_overrides.json | 26 +++++ 25 files changed, 683 insertions(+), 146 deletions(-) diff --git a/AppImage/components/audit-comparison.tsx b/AppImage/components/audit-comparison.tsx index 55a33f5a..0718e5e9 100644 --- a/AppImage/components/audit-comparison.tsx +++ b/AppImage/components/audit-comparison.tsx @@ -151,7 +151,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: { {error &&

{error}

} {!comparable ? ( -

+

{baseline ? t("audit.comparison.isBaseline") : t("audit.comparison.noBaseline")}

) : ( @@ -165,8 +165,8 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: { > {open ? : } - - + + {t("audit.comparison.since", { date: baseline?.started_at ? new Date(baseline.started_at * 1000).toLocaleDateString(language) @@ -174,12 +174,12 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: { })} {counts.length === 0 ? ( - + {t("audit.comparison.noChange")} ) : counts.map(({ key, Icon, tone, n }) => ( - - + + {t(`audit.comparison.${key}`)} {n} @@ -191,7 +191,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: { {GROUPS.filter((g) => (comparison[g.key] || []).length > 0).map( ({ key, Icon, tone }) => (
-

+

{t(`audit.comparison.${key}`)} @@ -209,7 +209,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: { ), )} {(comparison.unchanged || []).length > 0 && ( -

+

{t("audit.comparison.unchanged", { count: String(comparison.unchanged.length), })} diff --git a/AppImage/components/audit-report.tsx b/AppImage/components/audit-report.tsx index f24e2da0..a90b3ac3 100644 --- a/AppImage/components/audit-report.tsx +++ b/AppImage/components/audit-report.tsx @@ -15,7 +15,6 @@ import { } from "lucide-react" import { fetchApi } from "../lib/api-config" import { useT } from "../lib/i18n/provider" -import { AuditInventory } from "./audit-inventory" import { AuditPolicy } from "./audit-policy" import { AuditChanges } from "./audit-changes" import { AuditComparison } from "./audit-comparison" @@ -106,7 +105,7 @@ export function AuditReport() { const { language } = useI18n() // Assessment and inventory answer different questions and are // read differently: one is triaged, the other is read through. - const [view, setView] = useState<"assessment" | "inventory" | "changes" | "policy">("assessment") + const [view, setView] = useState<"assessment" | "changes" | "policy">("assessment") const [running, setRunning] = useState(false) const [latest, setLatest] = useState(null) const [findings, setFindings] = useState([]) @@ -123,7 +122,10 @@ export function AuditReport() { // The profile decides which question the page answers, so it governs // both what an assessment runs and what the inventory documents. const [profile, setProfile] = useState("full") - const [profiles, setProfiles] = useState>([]) + const [profiles, setProfiles] = useState>([]) + // Set when a Lynis-bearing assessment is about to run and the stored + // report is missing or stale: the user decides whether to run Lynis now. + const [lynisPrompt, setLynisPrompt] = useState(null) const [building, setBuilding] = useState(false) const loadRun = useCallback(async (runId: string) => { @@ -183,12 +185,13 @@ export function AuditReport() { return () => clearInterval(id) }, [running, refresh]) - const startRun = async () => { + const doRun = async (runLynis: boolean) => { + setLynisPrompt(null) setError(null) try { const data: any = await fetchApi("/api/audit/run", { method: "POST", - body: JSON.stringify({ profile }), + body: JSON.stringify({ profile, run_lynis: runLynis }), }) if (data?.success) setRunning(true) else setError(data?.message || t("audit.errors.runFailed")) @@ -197,6 +200,28 @@ export function AuditReport() { } } + // A profile that includes the Lynis check asks the user before running, + // since producing a fresh Lynis report takes a few minutes. When a + // recent report already exists — or Lynis is not installed — the run + // starts straight away and reuses it. + const startRun = async () => { + setError(null) + const spec = profiles.find((p) => p.id === profile) + const runsLynis = !!spec && (spec.areas === null || spec.areas.includes("security")) + if (runsLynis) { + try { + const r: any = await fetchApi("/api/audit/lynis-readiness") + if (r?.success && r.installed && (!r.has_report || r.stale)) { + setLynisPrompt({ ageDays: r.age_days ?? null, stale: !!r.stale }) + return + } + } catch { + /* Readiness is advisory; on failure run without Lynis rather than block. */ + } + } + doRun(false) + } + // Accepting or revoking changes which findings are active, so the run // is re-read afterwards rather than patched in place: the stored // finding is what the next report will show. @@ -308,10 +333,18 @@ export function AuditReport() { try { const inv: any = await fetchApi( `/api/audit/inventory?profile=${encodeURIComponent(profile)}`) + // A focused report shows only the checks its profile runs, even + // when the findings on screen came from a full assessment: the + // report is scoped to its question, not to whichever run produced + // the data. `areas: null` (full, diagnostic) keeps everything. + const spec = profiles.find((p) => p.id === profile) + const scopedFindings = !spec || spec.areas === null + ? findings + : findings.filter((f) => spec.areas!.includes(f.area) || spec.include.includes(f.check_id)) openAuditDocument({ profile, run: latest, - findings, + findings: scopedFindings, inventory: inv?.success ? inv.inventory : null, t, locale: language, @@ -371,7 +404,7 @@ export function AuditReport() { className="flex w-full rounded-lg border border-border bg-muted/40 p-1 gap-1 sm:inline-flex sm:w-auto" > - {(["assessment", "inventory", "changes", "policy"] as const).map((key) => ( + {(["assessment", "changes", "policy"] as const).map((key) => (

- ) - } - return (
@@ -742,6 +751,30 @@ export function AuditReport() { })}
+ !o && setLynisPrompt(null)}> + + + {t("audit.lynis.title")} + + {lynisPrompt?.stale + ? t("audit.lynis.bodyStale", { days: String(lynisPrompt?.ageDays ?? "") }) + : t("audit.lynis.bodyNotRun")} + + + + + + + + + + !o && setAccepting(null)}> diff --git a/AppImage/components/latency-detail-modal.tsx b/AppImage/components/latency-detail-modal.tsx index 49c28d21..5a40c7d5 100644 --- a/AppImage/components/latency-detail-modal.tsx +++ b/AppImage/components/latency-detail-modal.tsx @@ -363,10 +363,12 @@ const generateLatencyReport = (report: ReportData, t: TFunction) => { .top-bar-title { font-weight: 600; } .top-bar-subtitle { font-size: 11px; color: #94a3b8; display: none; } .top-bar button { - background: #06b6d4; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; - font-size: 14px; font-weight: 600; cursor: pointer; + background: #06b6d4; color: #fff; border: none; padding: 8px 12px; border-radius: 6px; + font-size: 14px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .top-bar button:hover { background: #0891b2; } + .top-bar .btn-group { display: flex; gap: 8px; } + .top-bar button svg { width: 18px; height: 18px; display: block; } @media (min-width: 640px) { .top-bar { padding: 12px 24px; } .top-bar-subtitle { display: block; } @@ -532,7 +534,10 @@ const generateLatencyReport = (report: ReportData, t: TFunction) => {
${t("network.latency.report.topBarSubtitle")}
- +
+ + +
diff --git a/AppImage/components/release-notes-modal.tsx b/AppImage/components/release-notes-modal.tsx index f21b7cfa..386a1a8b 100644 --- a/AppImage/components/release-notes-modal.tsx +++ b/AppImage/components/release-notes-modal.tsx @@ -305,34 +305,34 @@ export const CHANGELOG: Record = { // that haven't been curated by hand. const CURRENT_VERSION_FEATURES = [ { - icon: , - key: "releaseNotes.currentFeatures.aiCustomEndpoint", - text: "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).", + icon: , + key: "releaseNotes.currentFeatures.auditAssessment", + text: "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", }, { - icon: , - key: "releaseNotes.currentFeatures.secureGatewayArch", - text: "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).", + icon: , + key: "releaseNotes.currentFeatures.changeJournal", + text: "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + }, + { + icon: , + key: "releaseNotes.currentFeatures.auditReports", + text: "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + }, + { + icon: , + key: "releaseNotes.currentFeatures.auditPolicyBaseline", + text: "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", }, { icon: , - key: "releaseNotes.currentFeatures.atomicNotifications", - text: "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", - }, - { - icon: , - key: "releaseNotes.currentFeatures.borgSshPort", - text: "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", + key: "releaseNotes.currentFeatures.groupedAppUpdates", + text: "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", }, { icon: , - key: "releaseNotes.currentFeatures.githubToken", - text: "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - }, - { - icon: , - key: "releaseNotes.currentFeatures.replicationContext", - text: "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + key: "releaseNotes.currentFeatures.adminTokenScope", + text: "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n).", }, ] diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index eece61be..623b7c5d 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -21,6 +21,7 @@ import { useI18n } from "../lib/i18n/provider" interface ApiTokenEntry { id: string name: string + scope?: "read_only" | "full_admin" token_prefix: string created_at: string expires_at: string @@ -130,6 +131,10 @@ export function Security() { const [loadingTokens, setLoadingTokens] = useState(false) const [revokingTokenId, setRevokingTokenId] = useState(null) const [tokenName, setTokenName] = useState("") + // API tokens default to read-only (the safe choice for dashboards that + // only read metrics). full_admin is opt-in and carries a warning: it + // can do everything the logged-in user can (host power, updates, terminal). + const [tokenScope, setTokenScope] = useState<"read_only" | "full_admin">("read_only") // Proxmox Firewall state const [firewallLoading, setFirewallLoading] = useState(true) @@ -1152,6 +1157,7 @@ export function Security() { password: tokenPassword, totp_token: totpEnabled ? tokenTotpCode : undefined, token_name: tokenName || st("apiTokens.defaultName"), + scope: tokenScope, }), }) @@ -1170,6 +1176,7 @@ export function Security() { setTokenPassword("") setTokenTotpCode("") setTokenName("") + setTokenScope("read_only") loadApiTokens() } catch (err) { setError(err instanceof Error ? err.message : st("errors.generateTokenRetry")) @@ -1346,10 +1353,12 @@ export function Security() { .top-bar-title { font-weight: 600; } .top-bar-subtitle { font-size: 11px; color: #94a3b8; display: none; } .top-bar button { - background: #06b6d4; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; - font-size: 14px; font-weight: 600; cursor: pointer; + background: #06b6d4; color: #fff; border: none; padding: 8px 12px; border-radius: 6px; + font-size: 14px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .top-bar button:hover { background: #0891b2; } + .top-bar .btn-group { display: flex; gap: 8px; } + .top-bar button svg { width: 18px; height: 18px; display: block; } .hide-mobile { } @media (min-width: 640px) { .top-bar { padding: 12px 24px; } @@ -1472,7 +1481,10 @@ function pmxPrint(){ ${st("lynis.report.brandTitle")} ${st("lynis.report.reviewHint")} - +
+ + +
@@ -2478,6 +2490,44 @@ ${(report.sections && report.sections.length > 0) ? ` +
+ +
+ + +
+ {tokenScope === "full_admin" && ( +
+ + {st("apiTokens.scope.fullAdminWarning")} +
+ )} +
+
@@ -2650,6 +2700,15 @@ ${(report.sections && report.sections.length > 0) ? `

{token.name}

+ {token.scope === "read_only" ? ( + + {st("apiTokens.scope.readOnly")} + + ) : ( + + {st("apiTokens.scope.fullAdmin")} + + )} {isInvalid && ( {st("apiTokens.invalidRegenerate")} diff --git a/AppImage/lib/audit-document.ts b/AppImage/lib/audit-document.ts index 6f5cd83c..04dbe8ef 100644 --- a/AppImage/lib/audit-document.ts +++ b/AppImage/lib/audit-document.ts @@ -892,6 +892,93 @@ function scopeSection(input: DocumentInput, n: number): string { // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Focused reports: a posture header and the one panel that answers the +// report's question. A focused report opens on its verdict, not on what +// the machine is — the inventory is its own report. +// --------------------------------------------------------------------------- + +/** The verdict a focused report opens on: the counts that bear on its + * question, phrased in its own terms. The counts are already scoped, + * because a focused run only ran that profile's checks. */ +function postureHeader(input: DocumentInput, n: number): string { + const { findings, t, locale } = input + const counts: Record = {} + for (const f of findings) { const c = shownAs(f); counts[c] = (counts[c] || 0) + 1 } + const fails = counts.critical || 0 + const warns = counts.warning || 0 + const applicable = findings.filter(f => f.classification !== "not_applicable") + const verified = applicable.filter(f => !f.incomplete && + ["critical", "warning", "observation", "conformant", "accepted"].includes(f.classification)).length + const incomplete = verified < applicable.length || !!(input.run && !input.run.finished_at) + const state = fails ? "critical" : warns ? "warning" + : counts.observation ? "observation" : "conformant" + const headline = t(`audit.document.posture.${input.profile}`, { + critical: String(fails), warning: String(warns), + observation: String(counts.observation || 0), + }) + const body = ` +
+
+

${icon("summary", 22, CLASS_COLOR[state])}${esc(t(`audit.profile.${input.profile}`))}

+

${esc(headline)}

+ ${incomplete ? `

${esc(auditLabel(t, "incomplete"))}

` : ""} +

+ ${esc(t("audit.document.runAt", { date: when(input.run?.started_at, locale) }))} +

+
+
+
${[ + card(t("audit.classifications.critical"), String(fails), { center: true, color: CLASS_COLOR.critical }), + card(t("audit.classifications.warning"), String(warns), { center: true, color: CLASS_COLOR.warning }), + card(t("audit.classifications.observation"), String(counts.observation || 0), { center: true, color: CLASS_COLOR.observation }), + card(t("audit.classifications.conformant"), String(counts.conformant || 0), { center: true, color: CLASS_COLOR.conformant }), + ].join("")}
` + return section(n, t("audit.document.postureTitle"), body, "summary") +} + +/** Backup coverage: the signature panel of the backup report — how many + * guests carry a job, drawn as a meter with the guests that carry none. */ +function backupCoveragePanel(input: DocumentInput, n: number): string { + const s = input.inventory?.sections || {} + const guests = s.guests || [] + const { t } = input + if (!guests.length) return "" + const unprotected = guests.filter((g: any) => !(g.backups || []).length) + const selected = guests.length - unprotected.length + const fraction = guests.length ? selected / guests.length * 100 : 0 + const diagram = storageDiagram(guests, { + guests: t("audit.inventory.guests"), storage: t("audit.document.storage"), + backup: t("audit.document.backupDestination"), unprotected: auditLabel(t, "noJob"), + }) + const body = ` +

${icon("storage")}${esc(auditLabel(t, "coverage"))}

+
+
${selected} / ${guests.length} · ${esc(auditLabel(t, "scheduled"))}${unprotected.length} · ${esc(auditLabel(t, "noJob"))}
+

${esc(auditLabel(t, "copyScope"))}

+ ${diagram ? `

${esc(t("audit.document.storageDiagramNote"))}

${diagram}
` : ""} + ${unprotected.length + ? callout("info", t("audit.document.unprotectedGuests", { count: String(unprotected.length) }), + esc(unprotected.map((g: any) => `${g.vmid} ${g.name}`).join(" · "))) + : callout("info", auditLabel(t, "scheduled"), esc(auditLabel(t, "copyScope")))}` + return section(n, auditLabel(t, "coverage"), body, "storage") +} + +/** Capacity meters: the signature panel of the capacity report — used + * against total per connected storage, read from the check's evidence. */ +function capacityMetersPanel(input: DocumentInput, n: number): string { + const { t } = input + const finding = input.findings.find(f => f.check_id === "storage.connected_storage") + let capacityRows: any[] = [] + try { capacityRows = JSON.parse(finding?.evidence || "{}").storages || [] } catch { /* Raw evidence stays in the appendix. */ } + const meters = capacityRows.filter(r => Number(r.total) > 0 && r.used != null).map(r => { + const ratio = Math.max(0, Math.min(100, Number(r.used) / Number(r.total) * 100)) + return `
${esc(r.storage)}${esc(bytes(Number(r.used)))} / ${esc(bytes(Number(r.total)))}
` + }).join("") + if (!meters) return "" + return section(n, auditLabel(t, "capacity"), meters, "storage") +} + export function buildAuditDocument(input: DocumentInput): string { const { t, locale } = input const node = input.inventory?.sections?.identity?.node || t("audit.document.unknownNode") @@ -905,6 +992,10 @@ export function buildAuditDocument(input: DocumentInput): string { // no checks, so an assessment summary above it counted nothing and a // findings section below it listed nothing: two empty frames around // the only thing the reader opened this for. + // The inventory is its own report: an assessment — the whole audit or + // a focused one — opens on its verdict and prints no structure tables. + // A focused report adds the one panel that answers its question, and + // the inventory profile is the only one that documents the machine. const builders = input.profile === "inventory" ? [ identitySection, clusterSection, architectureSection, disksSection, @@ -913,11 +1004,13 @@ export function buildAuditDocument(input: DocumentInput): string { ] : input.profile === "diagnostic" ? [diagnosticSummary, actionsSection, unreadSection, scopeSection] - : [ - executiveSummary, identitySection, clusterSection, architectureSection, - disksSection, networkSection, latencySection, storageSection, guestsSection, - passthroughSection, proxmenuxSection, findingsSection, scopeSection, evidenceSection, - ] + : input.profile === "security" + ? [postureHeader, findingsSection, scopeSection, evidenceSection] + : input.profile === "backup" + ? [postureHeader, backupCoveragePanel, findingsSection, scopeSection, evidenceSection] + : input.profile === "capacity" + ? [postureHeader, capacityMetersPanel, disksSection, findingsSection, scopeSection, evidenceSection] + : [executiveSummary, findingsSection, scopeSection, evidenceSection] // A section a profile did not ask for produces nothing, and the // numbering closes over the gap rather than skipping a number. Each diff --git a/AppImage/lib/report-diagrams.ts b/AppImage/lib/report-diagrams.ts index cee026b9..0bb9bf94 100644 --- a/AppImage/lib/report-diagrams.ts +++ b/AppImage/lib/report-diagrams.ts @@ -93,11 +93,13 @@ const DEFS = ` ` function svg(width: number, height: number, body: string): string { - // A viewBox with no fixed width lets the diagram scale to the column on - // screen and to the page when printed, without a second layout. + // A viewBox with no fixed width lets the diagram scale down to a narrow + // column, but `max-width` caps it at its own coordinate space so a + // diagram with few elements is not scaled up until its boxes and text + // fill the page. Centred, so a capped diagram sits under its heading. return `${DEFS}${body}` + style="display:block;height:auto;max-width:${width}px;margin-inline:auto">${DEFS}${body}` } /** @@ -113,7 +115,10 @@ export function networkDiagram( const entries = Object.entries(bridges || {}) if (entries.length === 0) return "" - const COL_W = 132, BOX_H = 34, GAP_Y = 12, PAD = 12 + // COL_W is the column pitch and BOX_W the box itself: the difference + // between them is the horizontal air between a box and the next, drawn + // as the arrow. A wider pitch spreads the columns apart. + const COL_W = 180, BOX_W = 116, BOX_H = 34, GAP_Y = 12, PAD = 12 const rows: Array<{ nics: Node[]; bond: Node | null; bridge: Node; count: number }> = [] for (const [id, b] of entries) { @@ -140,7 +145,7 @@ export function networkDiagram( const height = PAD * 2 + rows.reduce((h, r) => h + Math.max(r.nics.length, 1) * (BOX_H + GAP_Y), 0) - const width = COL_W * (guestsCol + 1) + PAD * 2 + const width = PAD * 2 + COL_W * guestsCol + BOX_W let y = PAD const parts: string[] = [] @@ -149,7 +154,7 @@ export function networkDiagram( ? [labels.nic, labels.bond, labels.bridge, labels.guests] : [labels.nic, labels.bridge, labels.guests] parts.push(captions.map((c, i) => - `${esc(c.toUpperCase())}`).join("")) y += 8 @@ -160,20 +165,20 @@ export function networkDiagram( row.nics.forEach((n, i) => { const ny = y + i * (BOX_H + GAP_Y) - parts.push(box(PAD, ny, COL_W - 20, BOX_H, n)) + parts.push(box(PAD, ny, BOX_W, BOX_H, n)) const target = row.bond ? PAD + COL_W : PAD + COL_W * bridgeCol - parts.push(arrow(PAD + COL_W - 20, ny + BOX_H / 2, target, midY + BOX_H / 2)) + parts.push(arrow(PAD + BOX_W, ny + BOX_H / 2, target, midY + BOX_H / 2)) }) if (row.bond) { - parts.push(box(PAD + COL_W, midY, COL_W - 20, BOX_H, row.bond)) - parts.push(arrow(PAD + COL_W * bridgeCol - 20, midY + BOX_H / 2, + parts.push(box(PAD + COL_W, midY, BOX_W, BOX_H, row.bond)) + parts.push(arrow(PAD + COL_W + BOX_W, midY + BOX_H / 2, PAD + COL_W * bridgeCol, midY + BOX_H / 2)) } - parts.push(box(PAD + COL_W * bridgeCol, midY, COL_W - 20, BOX_H, row.bridge)) - parts.push(arrow(PAD + COL_W * guestsCol - 20, midY + BOX_H / 2, + parts.push(box(PAD + COL_W * bridgeCol, midY, BOX_W, BOX_H, row.bridge)) + parts.push(arrow(PAD + COL_W * bridgeCol + BOX_W, midY + BOX_H / 2, PAD + COL_W * guestsCol, midY + BOX_H / 2)) - parts.push(box(PAD + COL_W * guestsCol, midY, COL_W - 20, BOX_H, + parts.push(box(PAD + COL_W * guestsCol, midY, BOX_W, BOX_H, { id: `${row.bridge.id}-g`, label: String(row.count), sub: labels.guests })) y += block } diff --git a/AppImage/lib/report-shell.ts b/AppImage/lib/report-shell.ts index 55450510..d986f719 100644 --- a/AppImage/lib/report-shell.ts +++ b/AppImage/lib/report-shell.ts @@ -301,6 +301,7 @@ export function esc(value: unknown): string { /** Icon-only actions, as in the rest of the family: the browser's print * dialog exposes "Save as PDF" as a destination, so one button covers both. */ +const PRINTER_ICON = `` const PRINT_ICON = `` export interface ShellOptions { @@ -343,6 +344,7 @@ function pmxPrint(){ try { window.print(); } catch(e) {} } ${esc(o.topBarSubtitle || "")}
+
diff --git a/AppImage/lib/version.ts b/AppImage/lib/version.ts index 352d4fb1..e1e1ae5d 100644 --- a/AppImage/lib/version.ts +++ b/AppImage/lib/version.ts @@ -8,4 +8,4 @@ // 3. beta_version.txt ← bash pipeline (build_appimage.sh) // // Keep the three in sync on every bump. -export const APP_VERSION = "1.2.6" +export const APP_VERSION = "1.2.6.1-beta" diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json index 26df5fdc..51770872 100644 --- a/AppImage/messages/de/common.json +++ b/AppImage/messages/de/common.json @@ -2469,7 +2469,15 @@ "legacy": "Vermächtnis", "revoke": "Widerrufen", "loading": "Token werden geladen...", - "empty": "Noch keine API-Tokens." + "empty": "Noch keine API-Tokens.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Firewall", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5812,7 +5826,13 @@ "diagnosticUnread": "Nicht durchführbare Messungen", "diagnosticMoreRows": "{count} weitere Zeile(n), in der vollständigen Prüfung.", "structureTitle": "Struktur und Konfiguration", - "structureSubtitle": "Wie {node} aufgebaut und konfiguriert ist" + "structureSubtitle": "Wie {node} aufgebaut und konfiguriert ist", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Ergebnisse", "classifications": { @@ -5959,6 +5979,14 @@ "insufficient_runs": "Ein Vergleich braucht einen Referenzlauf und einen späteren; bisher ist nur einer aufgezeichnet" } }, - "notApplicableScope": "Im geprüften Umfang gibt es nichts, worauf diese Prüfung zutrifft." + "notApplicableScope": "Im geprüften Umfang gibt es nichts, worauf diese Prüfung zutrifft.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index b687d528..91ce00b3 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -2468,7 +2468,15 @@ "legacy": "Legacy", "revoke": "Revoke", "loading": "Loading tokens...", - "empty": "No API tokens yet." + "empty": "No API tokens yet.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Firewall", @@ -3264,7 +3272,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5878,7 +5892,13 @@ "diagnosticUnread": "Readings that could not be taken", "diagnosticMoreRows": "{count} further row(s), in the full audit.", "structureTitle": "Structure and configuration", - "structureSubtitle": "How {node} is built and configured" + "structureSubtitle": "How {node} is built and configured", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Results", "classifications": { @@ -6025,6 +6045,14 @@ "insufficient_runs": "A comparison needs a reference run and a later one; only one is recorded so far" } }, - "notApplicableScope": "Nothing in the inspected scope this check applies to." + "notApplicableScope": "Nothing in the inspected scope this check applies to.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json index 709a5eb0..3f04b69b 100644 --- a/AppImage/messages/es/common.json +++ b/AppImage/messages/es/common.json @@ -1937,7 +1937,7 @@ "notificationCategories": "Categorías de notificación", "enableTelegram": "Habilitar telegrama", "setupGuide": "+guía de configuración", - "botToken": "Ficha de robot", + "botToken": "Token del bot", "chatId": "ID de chat", "topicId": "ID de tema", "optional": "opcional", @@ -1947,7 +1947,7 @@ "sendTest": "Enviar prueba", "enableGotify": "Habilitar Gotify", "serverUrl": "URL del servidor", - "appToken": "Ficha de aplicación", + "appToken": "Token de la aplicación", "enableDiscord": "Activar Discord", "webhookUrl": "URL de webhook", "enableEmail": "Habilitar correo electrónico", @@ -2437,8 +2437,8 @@ "changesRestart": "La página puede desconectarse brevemente mientras se reinicia el servicio." }, "apiTokens": { - "defaultName": "Ficha del panel de control", - "title": "Fichas API", + "defaultName": "Token del panel de control", + "title": "Tokens API", "description": "Cree tokens de larga duración para integraciones y automatización.", "aboutTitle": "Acerca de los tokens API", "validFor": "Los tokens siguen siendo válidos hasta que los revoques.", @@ -2453,23 +2453,31 @@ "tokenName": "Nombre del token", "tokenNamePlaceholder": "Panel de la página de inicio", "generating": "Generando...", - "generate": "Generar ficha", + "generate": "Generar token", "yourToken": "Tu token API", "saveTokenNow": "Guarde este token ahora.", "tokenOnlyShownOnce": "Sólo se mostrará una vez.", - "token": "Simbólico", + "token": "Token", "copied": "Copiado", "howToUse": "como usarlo", "addToHeaders": "Agregue este encabezado a sus solicitudes:", - "authorizationHeaderExample": "Autorización: Portador ", + "authorizationHeaderExample": "Authorization: Bearer ", "readmeExamples": "Consulte el archivo README para ver ejemplos de integración.", "done": "Hecho", - "activeTokens": "Fichas activas", + "activeTokens": "Tokens activos", "invalidRegenerate": "Si se pierde un token, revocarlo y generar uno nuevo.", "legacy": "Legado", "revoke": "Revocar", - "loading": "Cargando fichas...", - "empty": "Aún no hay tokens API." + "loading": "Cargando tokens...", + "empty": "Aún no hay tokens API.", + "scope": { + "label": "Permisos del token", + "readOnly": "Solo lectura", + "readOnlyHint": "Consulta métricas y estado. Recomendado para paneles e integraciones.", + "fullAdmin": "Administrador completo", + "fullAdminHint": "Control total, igual que tu sesión.", + "fullAdminWarning": "Un token de administrador completo puede hacer todo lo que tú: apagar o reiniciar el host, lanzar actualizaciones y abrir un terminal. Compártelo solo con integraciones de total confianza." + } }, "firewall": { "title": "Cortafuegos", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Los eventos de notificación reservan su huella de deduplicación de forma atómica antes del procesado por IA y del envío por canal, así que colectores concurrentes, callbacks de finalización o procesos Monitor paralelos no pueden enviar el mismo evento dos veces. La reserva se libera cuando ningún canal tiene éxito, preservando los reintentos.", "borgSshPort": "Destino remoto Borg — el diálogo Añadir destino Borg y el TUI del shell aceptan un puerto SSH personalizado. BORG_RSH, el flujo de instalación automática de clave y la sonda de capacidad lo respetan. Totalmente retrocompatible con las entradas existentes creadas sin un puerto explícito (sugerido por @songochain en la discusión #236).", "githubToken": "Settings → GitHub API acepta un token de acceso personal opcional para las comprobaciones de releases y tags cuando se agota la cuota anónima. El token se guarda cifrado y nunca se devuelve al navegador; el error de rate limit está traducido en todos los idiomas del Monitor (sugerido por @SystemIdleProcess en la discusión #306).", - "replicationContext": "Las notificaciones nativas de fallo de replicación de Proxmox ahora resuelven el ID del trabajo de replicación, el ID de la VM/LXC afectada y el nombre del guest; el bloque de error exacto de Proxmox se conserva como motivo, y cada trabajo de replicación deduplica de forma independiente (reportado por Ale R.)." + "replicationContext": "Las notificaciones nativas de fallo de replicación de Proxmox ahora resuelven el ID del trabajo de replicación, el ID de la VM/LXC afectada y el nombre del guest; el bloque de error exacto de Proxmox se conserva como motivo, y cada trabajo de replicación deduplica de forma independiente (reportado por Ale R.).", + "auditAssessment": "Auditoría e Informes — una nueva página que documenta y evalúa el nodo. La evaluación ejecuta un catálogo de comprobaciones de seguridad, backups, capacidad, almacenamiento, red, hardware, invitados y sistema, clasificando cada hallazgo como crítico, aviso, observación o conforme, y diciendo qué leyó en lugar de juzgarlo.", + "changeJournal": "Diario de cambios — la vista Cambios muestra exactamente lo que ProxMenux modificó en el host: cada fichero de configuración, paquete y servicio que tocó, con el estado anterior de cada uno, deduplicado a una vista de estado actual que refleja el host tal como está ahora, no un registro de cada ejecución.", + "auditReports": "Informes — una auditoría completa más informes enfocados de Revisión de seguridad, Garantía de backups y Capacidad y desgaste, cada uno con su propia cabecera de postura, y un Inventario imprimible; todos se exportan a PDF. La evaluación de seguridad pregunta antes de ejecutar Lynis para que sea rápida.", + "auditPolicyBaseline": "Política y referencia — declara qué se espera del host (backups requeridos, firewall, acceso SSH de root) para que los hallazgos se evalúen contra ello, marca una ejecución como referencia y ve qué cambió desde entonces, con los hallazgos nuevos, resueltos y aceptados por separado.", + "groupedAppUpdates": "Notificaciones de actualización de aplicaciones agrupadas — un mensaje completo por escaneo en lugar de uno por aplicación, agrupado por LXC con las versiones instalada y disponible.", + "adminTokenScope": "Ámbito administrativo — abrir un terminal del Monitor y desactivar la autenticación ahora requieren un token de administrador completo, así que un token de API de solo lectura se mantiene de solo lectura (reportado por @f3rs3n)." } }, "network": { @@ -5812,7 +5826,13 @@ "diagnosticUnread": "Lecturas que no se pudieron tomar", "diagnosticMoreRows": "{count} fila(s) más, en la auditoría completa.", "structureTitle": "Estructura y configuración", - "structureSubtitle": "Cómo está construido y configurado {node}" + "structureSubtitle": "Cómo está construido y configurado {node}", + "postureTitle": "Postura", + "posture": { + "security": "Exposición y acceso del host.", + "backup": "Protección de los invitados y su alcance real.", + "capacity": "Margen de crecimiento y desgaste de los discos." + } }, "results": "Resultados", "classifications": { @@ -5959,6 +5979,14 @@ "insufficient_runs": "Comparar exige una ejecución de referencia y otra posterior; por ahora solo hay una registrada" } }, - "notApplicableScope": "Nada en el alcance examinado al que esta comprobación aplique." + "notApplicableScope": "Nada en el alcance examinado al que esta comprobación aplique.", + "lynis": { + "title": "Ejecutar Lynis", + "bodyNotRun": "Lynis está instalado pero aún no se ha ejecutado. Ejecutarlo ahora completa la revisión de seguridad, pero el proceso puede tardar unos minutos.", + "bodyStale": "El informe de Lynis es de hace {days} días. Puedes ejecutarlo ahora para actualizar los datos (tarda un poco más) o continuar con el informe existente.", + "withLynis": "Ejecutar con Lynis", + "withoutLynis": "Ejecutar sin Lynis", + "cancel": "Cancelar" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json index 5cc86ab5..a8fd1789 100644 --- a/AppImage/messages/fr/common.json +++ b/AppImage/messages/fr/common.json @@ -2469,7 +2469,15 @@ "legacy": "Héritage", "revoke": "Révoquer", "loading": "Chargement des jetons...", - "empty": "Aucun jeton API pour l'instant." + "empty": "Aucun jeton API pour l'instant.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Pare-feu", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5812,7 +5826,13 @@ "diagnosticUnread": "Relevés impossibles à prendre", "diagnosticMoreRows": "{count} ligne(s) de plus, dans l'audit complet.", "structureTitle": "Structure et configuration", - "structureSubtitle": "Comment {node} est construit et configuré" + "structureSubtitle": "Comment {node} est construit et configuré", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Résultats", "classifications": { @@ -5959,6 +5979,14 @@ "insufficient_runs": "Une comparaison exige une exécution de référence et une postérieure ; une seule est enregistrée pour l'instant" } }, - "notApplicableScope": "Rien dans le périmètre examiné auquel cette vérification s'applique." + "notApplicableScope": "Rien dans le périmètre examiné auquel cette vérification s'applique.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json index 04c8e013..1c6f4afd 100644 --- a/AppImage/messages/it/common.json +++ b/AppImage/messages/it/common.json @@ -2469,7 +2469,15 @@ "legacy": "Eredità", "revoke": "Revocare", "loading": "Caricamento token...", - "empty": "Nessun token API ancora." + "empty": "Nessun token API ancora.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Firewall", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5812,7 +5826,13 @@ "diagnosticUnread": "Letture non eseguibili", "diagnosticMoreRows": "{count} riga/righe in più, nell'audit completo.", "structureTitle": "Struttura e configurazione", - "structureSubtitle": "Com'è costruito e configurato {node}" + "structureSubtitle": "Com'è costruito e configurato {node}", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Risultati", "classifications": { @@ -5959,6 +5979,14 @@ "insufficient_runs": "Un confronto richiede un'esecuzione di riferimento e una successiva; finora ne è registrata una sola" } }, - "notApplicableScope": "Nulla nell'ambito esaminato a cui questo controllo si applichi." + "notApplicableScope": "Nulla nell'ambito esaminato a cui questo controllo si applichi.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json index f191d588..d67b4b6e 100644 --- a/AppImage/messages/pt/common.json +++ b/AppImage/messages/pt/common.json @@ -2469,7 +2469,15 @@ "legacy": "Legado", "revoke": "Revogar", "loading": "Carregando fichas...", - "empty": "Ainda não há tokens de API." + "empty": "Ainda não há tokens de API.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Firewall", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5812,7 +5826,13 @@ "diagnosticUnread": "Leituras que não foi possível obter", "diagnosticMoreRows": "{count} linha(s) adicionais, na auditoria completa.", "structureTitle": "Estrutura e configuração", - "structureSubtitle": "Como {node} está construído e configurado" + "structureSubtitle": "Como {node} está construído e configurado", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Resultados", "classifications": { @@ -5959,6 +5979,14 @@ "insufficient_runs": "Uma comparação exige uma execução de referência e outra posterior; até agora só há uma registada" } }, - "notApplicableScope": "Nada no âmbito examinado a que esta verificação se aplique." + "notApplicableScope": "Nada no âmbito examinado a que esta verificação se aplique.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index 2503abed..e8850923 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -2468,7 +2468,15 @@ "legacy": "Starší", "revoke": "Zrušiť", "loading": "Načítavam tokeny...", - "empty": "Zatiaľ tu nie sú žiadne API tokeny." + "empty": "Zatiaľ tu nie sú žiadne API tokeny.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Firewall", @@ -3264,7 +3272,13 @@ "atomicNotifications": "Udalosti upozornení si ešte pred spracovaním AI a odoslaním do kanála atómovo rezervujú jedinečný odtlačok proti duplicitám. Súbežné kontroly, dokončovacie volania ani paralelné procesy Monitoru tak neodošlú tú istú udalosť dvakrát. Ak neodošle úspešne žiadny kanál, rezervácia sa uvoľní, aby mohli fungovať opakované pokusy.", "borgSshPort": "Cieľ Borg na vzdialenom serveri — dialóg „Pridať cieľ Borg“ aj textové rozhranie podporujú vlastný SSH port. Rešpektujú ho BORG_RSH, automatická inštalácia kľúča aj kontrola kapacity. Existujúce ciele bez uvedeného portu naďalej fungujú bez zmeny (navrhol @songochain v diskusii #236).", "githubToken": "V časti Nastavenia → GitHub API môžete zadať voliteľný osobný prístupový token, ak pri kontrole vydaní a značiek vyčerpáte anonymný limit. Token je v úložisku zašifrovaný a nikdy sa neposiela do prehliadača; hlásenie o prekročenom limite je preložené do každého jazyka Monitoru (navrhol @SystemIdleProcess v diskusii #306).", - "replicationContext": "Natívne upozornenia na zlyhanie replikácie v Proxmoxe teraz doplnia ID úlohy replikácie, dotknuté ID VM/LXC a názov virtuálneho stroja alebo kontajnera. Presný blok chyby z Proxmoxu ostáva zachovaný ako dôvod a každá replikačná úloha sa posudzuje samostatne, aby sa správne odstránili duplicity (nahlásil Ale R.)." + "replicationContext": "Natívne upozornenia na zlyhanie replikácie v Proxmoxe teraz doplnia ID úlohy replikácie, dotknuté ID VM/LXC a názov virtuálneho stroja alebo kontajnera. Presný blok chyby z Proxmoxu ostáva zachovaný ako dôvod a každá replikačná úloha sa posudzuje samostatne, aby sa správne odstránili duplicity (nahlásil Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5878,7 +5892,13 @@ "diagnosticUnread": "Merania, ktoré sa nepodarilo vykonať", "diagnosticMoreRows": "{count} ďalších riadkov, v úplnom audite.", "structureTitle": "Štruktúra a konfigurácia", - "structureSubtitle": "Ako je {node} postavený a nakonfigurovaný" + "structureSubtitle": "Ako je {node} postavený a nakonfigurovaný", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Výsledky", "classifications": { @@ -6025,6 +6045,14 @@ "insufficient_runs": "Porovnanie vyžaduje referenčné spustenie a neskoršie; zatiaľ je zaznamenané len jedno" } }, - "notApplicableScope": "V preskúmanom rozsahu nie je nič, na čo by sa táto kontrola vzťahovala." + "notApplicableScope": "V preskúmanom rozsahu nie je nič, na čo by sa táto kontrola vzťahovala.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json index e1b11c9d..27416d6b 100644 --- a/AppImage/messages/sv/common.json +++ b/AppImage/messages/sv/common.json @@ -2469,7 +2469,15 @@ "legacy": "Arv", "revoke": "Återkalla", "loading": "Laddar tokens...", - "empty": "Inga API-tokens ännu." + "empty": "Inga API-tokens ännu.", + "scope": { + "label": "Token permissions", + "readOnly": "Read-only", + "readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.", + "fullAdmin": "Full admin", + "fullAdminHint": "Full control, like your own session.", + "fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations." + } }, "firewall": { "title": "Brandvägg", @@ -3265,7 +3273,13 @@ "atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.", "borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).", "githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).", - "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)." + "replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).", + "auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.", + "changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.", + "auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.", + "auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.", + "groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.", + "adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)." } }, "network": { @@ -5813,7 +5827,13 @@ "diagnosticUnread": "Avläsningar som inte kunde göras", "diagnosticMoreRows": "{count} rad(er) till, i den fullständiga granskningen.", "structureTitle": "Struktur och konfiguration", - "structureSubtitle": "Hur {node} är byggd och konfigurerad" + "structureSubtitle": "Hur {node} är byggd och konfigurerad", + "postureTitle": "Posture", + "posture": { + "security": "Host exposure and access.", + "backup": "Guest protection and whether it is real.", + "capacity": "Room to grow and the wear on the disks." + } }, "results": "Resultat", "classifications": { @@ -5960,6 +5980,14 @@ "insufficient_runs": "En jämförelse kräver en referenskörning och en senare; hittills är bara en registrerad" } }, - "notApplicableScope": "Inget i det granskade omfånget som den här kontrollen gäller." + "notApplicableScope": "Inget i det granskade omfånget som den här kontrollen gäller.", + "lynis": { + "title": "Run Lynis", + "bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.", + "bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.", + "withLynis": "Run with Lynis", + "withoutLynis": "Run without Lynis", + "cancel": "Cancel" + } } -} +} \ No newline at end of file diff --git a/AppImage/package-lock.json b/AppImage/package-lock.json index 790b187b..dcffd893 100644 --- a/AppImage/package-lock.json +++ b/AppImage/package-lock.json @@ -1,12 +1,12 @@ { "name": "ProxMenux-Monitor", - "version": "1.2.6", + "version": "1.2.6.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ProxMenux-Monitor", - "version": "1.2.6", + "version": "1.2.6.1-beta", "dependencies": { "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "1.2.2", diff --git a/AppImage/package.json b/AppImage/package.json index 04397223..2a65396d 100644 --- a/AppImage/package.json +++ b/AppImage/package.json @@ -1,6 +1,6 @@ { "name": "ProxMenux-Monitor", - "version": "1.2.6", + "version": "1.2.6.1-beta", "description": "Proxmox System Monitoring Dashboard", "private": true, "scripts": { diff --git a/AppImage/scripts/audit_checks.py b/AppImage/scripts/audit_checks.py index df97ffba..2e271915 100644 --- a/AppImage/scripts/audit_checks.py +++ b/AppImage/scripts/audit_checks.py @@ -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: diff --git a/AppImage/scripts/audit_checks_pve.py b/AppImage/scripts/audit_checks_pve.py index de4a8495..2fe6b2eb 100644 --- a/AppImage/scripts/audit_checks_pve.py +++ b/AppImage/scripts/audit_checks_pve.py @@ -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 = [] diff --git a/AppImage/scripts/auth_manager.py b/AppImage/scripts/auth_manager.py index 46255d89..dbfb96ae 100644 --- a/AppImage/scripts/auth_manager.py +++ b/AppImage/scripts/auth_manager.py @@ -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"), diff --git a/AppImage/scripts/flask_audit_routes.py b/AppImage/scripts/flask_audit_routes.py index 05bc7487..777726a4 100644 --- a/AppImage/scripts/flask_audit_routes.py +++ b/AppImage/scripts/flask_audit_routes.py @@ -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(): diff --git a/json/app_tracking_hints.json b/json/app_tracking_hints.json index 9d223bee..4bfe3966 100644 --- a/json/app_tracking_hints.json +++ b/json/app_tracking_hints.json @@ -506,6 +506,21 @@ "tag_regex": "(?i)(?:v|release[-_/]?)?(\\d+(?:\\.\\d+){1,3}(?:[-+._][0-9A-Za-z.-]+)?)", "website": "https://docs.certimate.me/en-US/" }, + "changedetection": { + "command_argv": [ + "/opt/changedetection/.venv/bin/changedetection.io", + "--version" + ], + "default_ports": [ + 5000 + ], + "github_source": "releases", + "installed_regex": "v?(\\d+\\.\\d+\\.\\d+)", + "installed_via": "command", + "repo": "dgtlmoon/changedetection.io", + "tag_regex": "v?(\\d+\\.\\d+\\.\\d+)", + "website": "https://changedetection.io/" + }, "checkmate": { "default_ports": [ 5173 diff --git a/json/runtime_verified_overrides.json b/json/runtime_verified_overrides.json index a971f5ac..dff11d3e 100644 --- a/json/runtime_verified_overrides.json +++ b/json/runtime_verified_overrides.json @@ -416,6 +416,32 @@ "label": "org.opencontainers.image.version" } ] + }, + "changedetection": { + "operational": true, + "name": "Changedetection.io", + "install_scope": [ + "community-script", + "manual" + ], + "evidence": [ + "community-confirmed:github-discussion-306" + ], + "detector": { + "installed_via": "command", + "command_argv": [ + "/opt/changedetection/.venv/bin/changedetection.io", + "--version" + ], + "installed_regex": "v?(\\d+\\.\\d+\\.\\d+)", + "repo": "dgtlmoon/changedetection.io", + "github_source": "releases", + "tag_regex": "v?(\\d+\\.\\d+\\.\\d+)" + }, + "default_ports": [ + 5000 + ], + "website": "https://changedetection.io/" } } }