mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
Focus the Audit & Report tab, scope API tokens
This commit is contained in:
@@ -151,7 +151,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: {
|
||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||
|
||||
{!comparable ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{baseline ? t("audit.comparison.isBaseline") : t("audit.comparison.noBaseline")}
|
||||
</p>
|
||||
) : (
|
||||
@@ -165,8 +165,8 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: {
|
||||
>
|
||||
{open ? <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
: <ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
|
||||
<TrendingUp className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<TrendingUp className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{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 }: {
|
||||
})}
|
||||
</span>
|
||||
{counts.length === 0 ? (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
{t("audit.comparison.noChange")}
|
||||
</Badge>
|
||||
) : counts.map(({ key, Icon, tone, n }) => (
|
||||
<Badge key={key} variant="outline" className={`text-xs gap-1.5 ${tone}`}>
|
||||
<Icon className="h-3 w-3" />
|
||||
<Badge key={key} variant="outline" className={`text-sm gap-1.5 ${tone}`}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{t(`audit.comparison.${key}`)}
|
||||
<span className="tabular-nums font-semibold">{n}</span>
|
||||
</Badge>
|
||||
@@ -191,7 +191,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: {
|
||||
{GROUPS.filter((g) => (comparison[g.key] || []).length > 0).map(
|
||||
({ key, Icon, tone }) => (
|
||||
<div key={key}>
|
||||
<p className={`flex items-center gap-1.5 text-xs font-medium mb-1 ${tone}`}>
|
||||
<p className={`flex items-center gap-1.5 text-sm font-medium mb-1 ${tone}`}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{t(`audit.comparison.${key}`)}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
@@ -209,7 +209,7 @@ export function AuditComparison({ runId, isBaseline, onBaselineSet }: {
|
||||
),
|
||||
)}
|
||||
{(comparison.unchanged || []).length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("audit.comparison.unchanged", {
|
||||
count: String(comparison.unchanged.length),
|
||||
})}
|
||||
|
||||
@@ -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<Run | null>(null)
|
||||
const [findings, setFindings] = useState<Finding[]>([])
|
||||
@@ -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<Array<{ id: string; runs_checks: boolean }>>([])
|
||||
const [profiles, setProfiles] = useState<Array<{ id: string; runs_checks: boolean; areas: string[] | null; include: string[] }>>([])
|
||||
// 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 | { ageDays: number | null; stale: boolean }>(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) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
@@ -427,30 +460,6 @@ export function AuditReport() {
|
||||
)
|
||||
}
|
||||
|
||||
if (view === "inventory") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* A row that cannot wrap has nowhere to put the controls but
|
||||
beside the title, which then squeezes into two lines. Title
|
||||
and controls are separate rows until there is width for both. */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClipboardCheck className="h-6 w-6 shrink-0 text-foreground" />
|
||||
<h2 className="text-xl lg:text-2xl font-bold text-foreground">{t("audit.title")}</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:ml-auto sm:flex-row sm:flex-wrap
|
||||
sm:items-center sm:gap-3">
|
||||
<div className="flex items-center gap-2 sm:contents">
|
||||
{profilePicker}{documentButton}
|
||||
</div>
|
||||
{viewTabs}
|
||||
</div>
|
||||
</div>
|
||||
<AuditInventory profile={profile} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||
@@ -742,6 +751,30 @@ export function AuditReport() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Dialog open={lynisPrompt !== null} onOpenChange={(o) => !o && setLynisPrompt(null)}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("audit.lynis.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{lynisPrompt?.stale
|
||||
? t("audit.lynis.bodyStale", { days: String(lynisPrompt?.ageDays ?? "") })
|
||||
: t("audit.lynis.bodyNotRun")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-row">
|
||||
<Button variant="outline" onClick={() => setLynisPrompt(null)}>
|
||||
{t("audit.lynis.cancel")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => doRun(false)}>
|
||||
{t("audit.lynis.withoutLynis")}
|
||||
</Button>
|
||||
<Button onClick={() => doRun(true)}>
|
||||
{t("audit.lynis.withLynis")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={accepting !== null} onOpenChange={(o) => !o && setAccepting(null)}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -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) => {
|
||||
<div class="top-bar-subtitle">${t("network.latency.report.topBarSubtitle")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="window.print()">${t("network.latency.report.printSavePdf")}</button>
|
||||
<div class="btn-group">
|
||||
<button onclick="window.print()" title="Print" aria-label="Print"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg></button>
|
||||
<button onclick="window.print()" title="Save as PDF" aria-label="Save as PDF"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
|
||||
@@ -305,34 +305,34 @@ export const CHANGELOG: Record<string, ReleaseNote> = {
|
||||
// that haven't been curated by hand.
|
||||
const CURRENT_VERSION_FEATURES = [
|
||||
{
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
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: <Activity className="h-5 w-5" />,
|
||||
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: <Wrench className="h-5 w-5" />,
|
||||
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: <Sliders className="h-5 w-5" />,
|
||||
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: <Sparkles className="h-5 w-5" />,
|
||||
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: <Calendar className="h-5 w-5" />,
|
||||
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: <Bell className="h-5 w-5" />,
|
||||
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: <DatabaseBackup className="h-5 w-5" />,
|
||||
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: <Shield className="h-5 w-5" />,
|
||||
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: <RefreshCw className="h-5 w-5" />,
|
||||
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).",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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<string | null>(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(){
|
||||
<strong>${st("lynis.report.brandTitle")}</strong>
|
||||
<span id="pmx-print-hint" class="hide-mobile" style="font-size:11px;opacity:0.7;">${st("lynis.report.reviewHint")}</span>
|
||||
</div>
|
||||
<button onclick="pmxPrint()">${st("lynis.printSavePdf")}</button>
|
||||
<div class="btn-group">
|
||||
<button onclick="pmxPrint()" title="Print" aria-label="Print"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg></button>
|
||||
<button onclick="pmxPrint()" title="Save as PDF" aria-label="Save as PDF"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
@@ -2478,6 +2490,44 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{st("apiTokens.scope.label")}</Label>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTokenScope("read_only")}
|
||||
disabled={generatingToken}
|
||||
className={`rounded-md border px-3 py-2 text-left transition-colors ${
|
||||
tokenScope === "read_only"
|
||||
? "border-blue-500 bg-blue-500/10"
|
||||
: "border-border hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
<span className="block text-sm font-medium">{st("apiTokens.scope.readOnly")}</span>
|
||||
<span className="block text-xs text-muted-foreground">{st("apiTokens.scope.readOnlyHint")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTokenScope("full_admin")}
|
||||
disabled={generatingToken}
|
||||
className={`rounded-md border px-3 py-2 text-left transition-colors ${
|
||||
tokenScope === "full_admin"
|
||||
? "border-amber-500 bg-amber-500/10"
|
||||
: "border-border hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
<span className="block text-sm font-medium">{st("apiTokens.scope.fullAdmin")}</span>
|
||||
<span className="block text-xs text-muted-foreground">{st("apiTokens.scope.fullAdminHint")}</span>
|
||||
</button>
|
||||
</div>
|
||||
{tokenScope === "full_admin" && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-2.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<TriangleAlert className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<span>{st("apiTokens.scope.fullAdminWarning")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="token-password">{st("auth.password")}</Label>
|
||||
<div className="relative">
|
||||
@@ -2650,6 +2700,15 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="text-sm font-medium truncate">{token.name}</p>
|
||||
{token.scope === "read_only" ? (
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-500/15 text-blue-500 border border-blue-500/30 whitespace-nowrap">
|
||||
{st("apiTokens.scope.readOnly")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-500/15 text-amber-500 border border-amber-500/30 whitespace-nowrap">
|
||||
{st("apiTokens.scope.fullAdmin")}
|
||||
</span>
|
||||
)}
|
||||
{isInvalid && (
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-500/15 text-red-500 border border-red-500/30 whitespace-nowrap">
|
||||
{st("apiTokens.invalidRegenerate")}
|
||||
|
||||
@@ -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<string, number> = {}
|
||||
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 = `
|
||||
<div class="exec-box posture-${esc(state)}">
|
||||
<div class="exec-text">
|
||||
<h3 class="audit-result-heading">${icon("summary", 22, CLASS_COLOR[state])}${esc(t(`audit.profile.${input.profile}`))}</h3>
|
||||
<p>${esc(headline)}</p>
|
||||
${incomplete ? `<p class="assessment-incomplete">${esc(auditLabel(t, "incomplete"))}</p>` : ""}
|
||||
<p style="font-size:11px;color:#64748b;margin-top:6px">
|
||||
${esc(t("audit.document.runAt", { date: when(input.run?.started_at, locale) }))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audit-counters">${[
|
||||
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("")}</div>`
|
||||
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 = `
|
||||
<div class="coverage-panel"><h3>${icon("storage")}${esc(auditLabel(t, "coverage"))}</h3>
|
||||
<div class="audit-meter"><span style="width:${fraction}%"></span></div>
|
||||
<div class="coverage-labels"><span>${selected} / ${guests.length} · ${esc(auditLabel(t, "scheduled"))}</span><span>${unprotected.length} · ${esc(auditLabel(t, "noJob"))}</span></div>
|
||||
<p class="muted">${esc(auditLabel(t, "copyScope"))}</p></div>
|
||||
${diagram ? `<div class="diagram"><p class="diagram-note">${esc(t("audit.document.storageDiagramNote"))}</p>${diagram}</div>` : ""}
|
||||
${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 `<div class="capacity-item"><strong>${esc(r.storage)}</strong><span>${esc(bytes(Number(r.used)))} / ${esc(bytes(Number(r.total)))}</span><div class="audit-meter"><span style="width:${ratio}%"></span></div></div>`
|
||||
}).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
|
||||
|
||||
@@ -93,11 +93,13 @@ const DEFS = `<defs>
|
||||
</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 `<svg viewBox="0 0 ${width} ${height}" width="100%" role="img"
|
||||
preserveAspectRatio="xMidYMin meet"
|
||||
style="display:block;height:auto">${DEFS}${body}</svg>`
|
||||
style="display:block;height:auto;max-width:${width}px;margin-inline:auto">${DEFS}${body}</svg>`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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) =>
|
||||
`<text x="${PAD + COL_W * i + COL_W / 2}" y="${PAD - 2}" text-anchor="middle"
|
||||
`<text x="${PAD + COL_W * i + BOX_W / 2}" y="${PAD - 2}" text-anchor="middle"
|
||||
font-size="9" font-weight="700" letter-spacing="0.06em"
|
||||
fill="${MUTED}">${esc(c.toUpperCase())}</text>`).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
|
||||
}
|
||||
|
||||
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>`
|
||||
const PRINT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>`
|
||||
|
||||
export interface ShellOptions {
|
||||
@@ -343,6 +344,7 @@ function pmxPrint(){ try { window.print(); } catch(e) {} }
|
||||
<span class="top-bar-subtitle">${esc(o.topBarSubtitle || "")}</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button onclick="pmxPrint()" title="Print" aria-label="Print">${PRINTER_ICON}</button>
|
||||
<button onclick="pmxPrint()" title="Save as PDF" aria-label="Save as PDF">${PRINT_ICON}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <token>",
|
||||
"authorizationHeaderExample": "Authorization: Bearer <token>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user