mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-25 02:47:46 +02:00
feat: add TLS certificate expiry check to HTTPS network monitors (#2401)
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
PlayCircleIcon,
|
||||
CopyIcon,
|
||||
CopyPlusIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "lucide-react"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import type { NetworkMonitorRecord, SystemRecord } from "@/types"
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Plural, Trans } from "@lingui/react/macro"
|
||||
import { $allSystemsById, $longestSystemName } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
@@ -37,9 +38,11 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { useMemo } from "react"
|
||||
import { formatBulkMonitorLine } from "@/components/network-monitors-table/monitor-dialog"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||
import { getCertDaysLeft, getCertExpiryLevel, getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||
import { pb } from "@/lib/api"
|
||||
|
||||
const certExpiryDotColors = { ok: "bg-green-500", warning: "bg-yellow-500", critical: "bg-red-500" }
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData, TValue> {
|
||||
label?: string
|
||||
@@ -249,6 +252,31 @@ export function getMonitorColumns(
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "cert",
|
||||
meta: { label: t`Certificate` },
|
||||
accessorFn: (record) => record.certInfo?.expires,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Certificate`} Icon={ShieldCheckIcon} />,
|
||||
cell: ({ row }) => {
|
||||
const { certInfo, system } = row.original
|
||||
const systemRecord = useStore($allSystemsById)[system]
|
||||
|
||||
if (!certInfo?.expires) {
|
||||
return <span className="ms-1.5 text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const daysLeft = getCertDaysLeft(certInfo)
|
||||
const color = isMuted(row.original, systemRecord)
|
||||
? "bg-muted-foreground/50"
|
||||
: certExpiryDotColors[getCertExpiryLevel(daysLeft)]
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums flex gap-2 items-center">
|
||||
<span className={cn("shrink-0 size-2 rounded-full", color)} />
|
||||
{daysLeft < 0 ? <Trans>Expired</Trans> : <Plural value={daysLeft} one="# day" other="# days" />}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "updated",
|
||||
meta: { label: t`Updated` },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||
import { getCertDaysLeft, getCertExpiryLevel, getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Plural, Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
@@ -37,14 +37,8 @@ import { isReadOnlyUser, queueUserSettings } from "@/lib/api"
|
||||
import { pb } from "@/lib/api"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { $allSystemsById, $direction, $userSettings, getUserChartTime } from "@/lib/stores"
|
||||
import {
|
||||
cn,
|
||||
isVisuallyLonger,
|
||||
matchesFilterGroups,
|
||||
parseFilterGroups,
|
||||
parseSemVer,
|
||||
} from "@/lib/utils"
|
||||
import type { ChartData, NetworkMonitorRecord } from "@/types"
|
||||
import { cn, formatShortDate, isVisuallyLonger, matchesFilterGroups, parseFilterGroups, parseSemVer } from "@/lib/utils"
|
||||
import type { ChartData, MonitorCertInfo, NetworkMonitorRecord } from "@/types"
|
||||
import { AddMonitorDialog, EditMonitorDialog } from "./monitor-dialog"
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
@@ -53,9 +47,11 @@ import {
|
||||
ArrowUpIcon,
|
||||
EthernetPortIcon,
|
||||
EyeIcon,
|
||||
LandmarkIcon,
|
||||
LoaderCircleIcon,
|
||||
ServerIcon,
|
||||
Settings2Icon,
|
||||
ShieldCheckIcon,
|
||||
XIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
@@ -636,6 +632,36 @@ function NetworkMonitorSheet({
|
||||
return <NetworkMonitorSheetContent key={monitor.system} open={open} onOpenChange={onOpenChange} monitor={monitor} />
|
||||
}
|
||||
|
||||
const certExpiryTextColors = { ok: "", warning: "text-yellow-600 dark:text-yellow-500", critical: "text-red-500" }
|
||||
|
||||
function CertExpiry({ cert }: { cert: MonitorCertInfo }) {
|
||||
const daysLeft = getCertDaysLeft(cert)
|
||||
const expires = formatShortDate(new Date(cert.expires).toISOString())
|
||||
const level = getCertExpiryLevel(daysLeft)
|
||||
return (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<ShieldCheckIcon className={cn("size-3.5 text-muted-foreground -me-1", certExpiryTextColors[level])} />
|
||||
<span className={certExpiryTextColors[level]}>
|
||||
{daysLeft < 0 ? (
|
||||
<Trans>Certificate expired {expires}</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Certificate expires {expires}
|
||||
</Trans>
|
||||
)}
|
||||
</span>
|
||||
{cert.issuer && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<LandmarkIcon className="size-3.5 text-muted-foreground -me-0.5" />
|
||||
<span>{cert.issuer}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NetworkMonitorSheetContent({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -683,7 +709,7 @@ function NetworkMonitorSheetContent({
|
||||
{system?.name ?? ""}
|
||||
</Link>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<ArrowLeftRightIcon className="size-3.5 text-muted-foreground" />
|
||||
<ArrowLeftRightIcon className="size-3.5 text-muted-foreground -me-0.5" />
|
||||
{monitor.protocol.toUpperCase()}
|
||||
{monitor.protocol === "tcp" && monitor.port > 0 && (
|
||||
<>
|
||||
@@ -692,6 +718,7 @@ function NetworkMonitorSheetContent({
|
||||
<span>{monitor.port}</span>
|
||||
</>
|
||||
)}
|
||||
{monitor.certInfo?.expires ? <CertExpiry cert={monitor.certInfo} /> : null}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid gap-4">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MonitorStats, NetworkMonitorRecord, RawMonitorStatsRecord } from "@/types"
|
||||
import type { MonitorCertInfo, MonitorStats, NetworkMonitorRecord, RawMonitorStatsRecord } from "@/types"
|
||||
|
||||
/** Derive chart metrics from the counts and response sum stored at every retention tier. */
|
||||
export function getMonitorStats(record: RawMonitorStatsRecord): MonitorStats {
|
||||
@@ -15,3 +15,15 @@ export function getMonitorTarget(monitor: Pick<NetworkMonitorRecord, "target" |
|
||||
const host = monitor.target.includes(":") && !monitor.target.startsWith("[") ? `[${monitor.target}]` : monitor.target
|
||||
return `${host}:${monitor.port}`
|
||||
}
|
||||
|
||||
/** Whole days until the certificate expires; negative once expired. */
|
||||
export function getCertDaysLeft(cert: Pick<MonitorCertInfo, "expires">, now = Date.now()) {
|
||||
return Math.floor((cert.expires - now) / 86_400_000)
|
||||
}
|
||||
|
||||
/** Expiry severity used for certificate colors. */
|
||||
export function getCertExpiryLevel(daysLeft: number): "ok" | "warning" | "critical" {
|
||||
if (daysLeft < 7) return "critical"
|
||||
if (daysLeft < 14) return "warning"
|
||||
return "ok"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ async function fetchMonitorStats(
|
||||
}
|
||||
|
||||
const NETWORK_MONITOR_FIELDS =
|
||||
"id,system,target,protocol,port,interval,res,resMin1h,resMax1h,resAvg1h,loss1h,enabled,updated"
|
||||
"id,system,target,protocol,port,interval,res,resMin1h,resMax1h,resAvg1h,loss1h,enabled,certInfo,updated"
|
||||
|
||||
interface UseNetworkMonitorsProps {
|
||||
systemId?: string
|
||||
|
||||
8
internal/site/src/types.d.ts
vendored
8
internal/site/src/types.d.ts
vendored
@@ -652,9 +652,17 @@ export interface NetworkMonitorRecord {
|
||||
loss1h: number
|
||||
interval: number
|
||||
enabled: boolean
|
||||
/** Latest TLS certificate details, reported for HTTPS targets. */
|
||||
certInfo?: MonitorCertInfo | null
|
||||
updated: string
|
||||
}
|
||||
|
||||
/** Leaf TLS certificate details reported by the agent. Timestamps are Unix milliseconds. */
|
||||
export interface MonitorCertInfo {
|
||||
expires: number
|
||||
issuer?: string
|
||||
}
|
||||
|
||||
/** Response times in microseconds and packet loss percentage (0-100). */
|
||||
export interface MonitorStats {
|
||||
res_avg: number
|
||||
|
||||
Reference in New Issue
Block a user