feat: add firewall status widget for server stats
This commit is contained in:
@@ -18,6 +18,7 @@ import { collectUptimeMetrics } from "./widgets/uptime-collector.js";
|
|||||||
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
||||||
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
||||||
import { collectLoginStats } from "./widgets/login-stats-collector.js";
|
import { collectLoginStats } from "./widgets/login-stats-collector.js";
|
||||||
|
import { collectFirewallMetrics } from "./widgets/firewall-collector.js";
|
||||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
|
||||||
async function resolveJumpHost(
|
async function resolveJumpHost(
|
||||||
@@ -1661,6 +1662,39 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let firewall: {
|
||||||
|
type: "iptables" | "nftables" | "none";
|
||||||
|
status: "active" | "inactive" | "unknown";
|
||||||
|
chains: Array<{
|
||||||
|
name: string;
|
||||||
|
policy: string;
|
||||||
|
rules: Array<{
|
||||||
|
chain: string;
|
||||||
|
target: string;
|
||||||
|
protocol: string;
|
||||||
|
source: string;
|
||||||
|
destination: string;
|
||||||
|
dport?: string;
|
||||||
|
sport?: string;
|
||||||
|
state?: string;
|
||||||
|
interface?: string;
|
||||||
|
extra?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
} = {
|
||||||
|
type: "none",
|
||||||
|
status: "unknown",
|
||||||
|
chains: [],
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
firewall = await collectFirewallMetrics(client);
|
||||||
|
} catch (e) {
|
||||||
|
statsLogger.debug("Failed to collect firewall metrics", {
|
||||||
|
operation: "firewall_metrics_failed",
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
cpu,
|
cpu,
|
||||||
memory,
|
memory,
|
||||||
@@ -1670,6 +1704,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
processes,
|
processes,
|
||||||
system,
|
system,
|
||||||
login_stats,
|
login_stats,
|
||||||
|
firewall,
|
||||||
};
|
};
|
||||||
|
|
||||||
metricsCache.set(host.id, result);
|
metricsCache.set(host.id, result);
|
||||||
|
|||||||
254
src/backend/ssh/widgets/firewall-collector.ts
Normal file
254
src/backend/ssh/widgets/firewall-collector.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import type { Client } from "ssh2";
|
||||||
|
import { execCommand } from "./common-utils.js";
|
||||||
|
import type {
|
||||||
|
FirewallMetrics,
|
||||||
|
FirewallChain,
|
||||||
|
FirewallRule,
|
||||||
|
} from "../../../types/stats-widgets.js";
|
||||||
|
|
||||||
|
function parseIptablesRule(line: string): FirewallRule | null {
|
||||||
|
if (!line.startsWith("-A ")) return null;
|
||||||
|
|
||||||
|
const rule: FirewallRule = {
|
||||||
|
chain: "",
|
||||||
|
target: "",
|
||||||
|
protocol: "all",
|
||||||
|
source: "0.0.0.0/0",
|
||||||
|
destination: "0.0.0.0/0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const chainMatch = line.match(/^-A\s+(\S+)/);
|
||||||
|
if (chainMatch) {
|
||||||
|
rule.chain = chainMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMatch = line.match(/-j\s+(\S+)/);
|
||||||
|
if (targetMatch) {
|
||||||
|
rule.target = targetMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolMatch = line.match(/-p\s+(\S+)/);
|
||||||
|
if (protocolMatch) {
|
||||||
|
rule.protocol = protocolMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceMatch = line.match(/-s\s+(\S+)/);
|
||||||
|
if (sourceMatch) {
|
||||||
|
rule.source = sourceMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const destMatch = line.match(/-d\s+(\S+)/);
|
||||||
|
if (destMatch) {
|
||||||
|
rule.destination = destMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dportMatch = line.match(/--dport\s+(\S+)/);
|
||||||
|
if (dportMatch) {
|
||||||
|
rule.dport = dportMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sportMatch = line.match(/--sport\s+(\S+)/);
|
||||||
|
if (sportMatch) {
|
||||||
|
rule.sport = sportMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateMatch = line.match(/--state\s+(\S+)/);
|
||||||
|
if (stateMatch) {
|
||||||
|
rule.state = stateMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interfaceMatch = line.match(/-i\s+(\S+)/);
|
||||||
|
if (interfaceMatch) {
|
||||||
|
rule.interface = interfaceMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIptablesOutput(output: string): FirewallChain[] {
|
||||||
|
const chains: Map<string, FirewallChain> = new Map();
|
||||||
|
const lines = output.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
const policyMatch = trimmed.match(/^:(\S+)\s+(\S+)/);
|
||||||
|
if (policyMatch) {
|
||||||
|
const [, chainName, policy] = policyMatch;
|
||||||
|
chains.set(chainName, {
|
||||||
|
name: chainName,
|
||||||
|
policy: policy,
|
||||||
|
rules: [],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rule = parseIptablesRule(trimmed);
|
||||||
|
if (rule) {
|
||||||
|
let chain = chains.get(rule.chain);
|
||||||
|
if (!chain) {
|
||||||
|
chain = {
|
||||||
|
name: rule.chain,
|
||||||
|
policy: "ACCEPT",
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
chains.set(rule.chain, chain);
|
||||||
|
}
|
||||||
|
chain.rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(chains.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNftablesOutput(output: string): FirewallChain[] {
|
||||||
|
const chains: FirewallChain[] = [];
|
||||||
|
let currentChain: FirewallChain | null = null;
|
||||||
|
|
||||||
|
const lines = output.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
const chainMatch = trimmed.match(
|
||||||
|
/chain\s+(\S+)\s*\{?\s*(?:type\s+\S+\s+hook\s+(\S+))?/,
|
||||||
|
);
|
||||||
|
if (chainMatch) {
|
||||||
|
if (currentChain) {
|
||||||
|
chains.push(currentChain);
|
||||||
|
}
|
||||||
|
currentChain = {
|
||||||
|
name: chainMatch[1].toUpperCase(),
|
||||||
|
policy: "ACCEPT",
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChain && trimmed.startsWith("policy ")) {
|
||||||
|
const policyMatch = trimmed.match(/policy\s+(\S+)/);
|
||||||
|
if (policyMatch) {
|
||||||
|
currentChain.policy = policyMatch[1].toUpperCase();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChain && trimmed && !trimmed.startsWith("}")) {
|
||||||
|
const rule: FirewallRule = {
|
||||||
|
chain: currentChain.name,
|
||||||
|
target: "",
|
||||||
|
protocol: "all",
|
||||||
|
source: "0.0.0.0/0",
|
||||||
|
destination: "0.0.0.0/0",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trimmed.includes("accept")) rule.target = "ACCEPT";
|
||||||
|
else if (trimmed.includes("drop")) rule.target = "DROP";
|
||||||
|
else if (trimmed.includes("reject")) rule.target = "REJECT";
|
||||||
|
|
||||||
|
const tcpMatch = trimmed.match(/tcp\s+dport\s+(\S+)/);
|
||||||
|
if (tcpMatch) {
|
||||||
|
rule.protocol = "tcp";
|
||||||
|
rule.dport = tcpMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const udpMatch = trimmed.match(/udp\s+dport\s+(\S+)/);
|
||||||
|
if (udpMatch) {
|
||||||
|
rule.protocol = "udp";
|
||||||
|
rule.dport = udpMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const saddrMatch = trimmed.match(/saddr\s+(\S+)/);
|
||||||
|
if (saddrMatch) {
|
||||||
|
rule.source = saddrMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const daddrMatch = trimmed.match(/daddr\s+(\S+)/);
|
||||||
|
if (daddrMatch) {
|
||||||
|
rule.destination = daddrMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const iifMatch = trimmed.match(/iif\s+"?(\S+)"?/);
|
||||||
|
if (iifMatch) {
|
||||||
|
rule.interface = iifMatch[1].replace(/"/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctStateMatch = trimmed.match(/ct\s+state\s+(\S+)/);
|
||||||
|
if (ctStateMatch) {
|
||||||
|
rule.state = ctStateMatch[1].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.target) {
|
||||||
|
currentChain.rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed === "}") {
|
||||||
|
if (currentChain) {
|
||||||
|
chains.push(currentChain);
|
||||||
|
currentChain = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChain) {
|
||||||
|
chains.push(currentChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chains;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectFirewallMetrics(
|
||||||
|
client: Client,
|
||||||
|
): Promise<FirewallMetrics> {
|
||||||
|
try {
|
||||||
|
const iptablesResult = await execCommand(
|
||||||
|
client,
|
||||||
|
"iptables-save 2>/dev/null",
|
||||||
|
15000,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (iptablesResult.stdout && iptablesResult.stdout.includes("*filter")) {
|
||||||
|
const chains = parseIptablesOutput(iptablesResult.stdout);
|
||||||
|
const hasRules = chains.some((c) => c.rules.length > 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "iptables",
|
||||||
|
status: hasRules ? "active" : "inactive",
|
||||||
|
chains: chains.filter(
|
||||||
|
(c) =>
|
||||||
|
c.name === "INPUT" || c.name === "OUTPUT" || c.name === "FORWARD",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nftResult = await execCommand(
|
||||||
|
client,
|
||||||
|
"nft list ruleset 2>/dev/null",
|
||||||
|
15000,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nftResult.stdout && nftResult.stdout.trim()) {
|
||||||
|
const chains = parseNftablesOutput(nftResult.stdout);
|
||||||
|
const hasRules = chains.some((c) => c.rules.length > 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "nftables",
|
||||||
|
status: hasRules ? "active" : "inactive",
|
||||||
|
chains,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "none",
|
||||||
|
status: "unknown",
|
||||||
|
chains: [],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
type: "none",
|
||||||
|
status: "unknown",
|
||||||
|
chains: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1714,7 +1714,25 @@
|
|||||||
"executingQuickAction": "Executing {{name}}...",
|
"executingQuickAction": "Executing {{name}}...",
|
||||||
"quickActionSuccess": "{{name}} completed successfully",
|
"quickActionSuccess": "{{name}} completed successfully",
|
||||||
"quickActionFailed": "{{name}} failed",
|
"quickActionFailed": "{{name}} failed",
|
||||||
"quickActionError": "Failed to execute {{name}}"
|
"quickActionError": "Failed to execute {{name}}",
|
||||||
|
"firewall": {
|
||||||
|
"title": "Firewall",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"notDetected": "Not Detected",
|
||||||
|
"policy": "Policy",
|
||||||
|
"rules": "rules",
|
||||||
|
"noRules": "No rules",
|
||||||
|
"noData": "No firewall data available",
|
||||||
|
"action": "Action",
|
||||||
|
"protocol": "Proto",
|
||||||
|
"port": "Port",
|
||||||
|
"source": "Source",
|
||||||
|
"accept": "ACCEPT",
|
||||||
|
"drop": "DROP",
|
||||||
|
"reject": "REJECT",
|
||||||
|
"anywhere": "Anywhere"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"tagline": "SSH SERVER MANAGER",
|
"tagline": "SSH SERVER MANAGER",
|
||||||
|
|||||||
@@ -6,7 +6,33 @@ export type WidgetType =
|
|||||||
| "uptime"
|
| "uptime"
|
||||||
| "processes"
|
| "processes"
|
||||||
| "system"
|
| "system"
|
||||||
| "login_stats";
|
| "login_stats"
|
||||||
|
| "firewall";
|
||||||
|
|
||||||
|
export interface FirewallRule {
|
||||||
|
chain: string;
|
||||||
|
target: string;
|
||||||
|
protocol: string;
|
||||||
|
source: string;
|
||||||
|
destination: string;
|
||||||
|
dport?: string;
|
||||||
|
sport?: string;
|
||||||
|
state?: string;
|
||||||
|
interface?: string;
|
||||||
|
extra?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallChain {
|
||||||
|
name: string;
|
||||||
|
policy: string;
|
||||||
|
rules: FirewallRule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallMetrics {
|
||||||
|
type: "iptables" | "nftables" | "none";
|
||||||
|
status: "active" | "inactive" | "unknown";
|
||||||
|
chains: FirewallChain[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatsConfig {
|
export interface StatsConfig {
|
||||||
enabledWidgets: WidgetType[];
|
enabledWidgets: WidgetType[];
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ export function HostManagerEditor({
|
|||||||
"processes",
|
"processes",
|
||||||
"system",
|
"system",
|
||||||
"login_stats",
|
"login_stats",
|
||||||
|
"firewall",
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.default([
|
.default([
|
||||||
@@ -3576,6 +3577,7 @@ export function HostManagerEditor({
|
|||||||
"processes",
|
"processes",
|
||||||
"system",
|
"system",
|
||||||
"login_stats",
|
"login_stats",
|
||||||
|
"firewall",
|
||||||
] as const
|
] as const
|
||||||
).map((widget) => (
|
).map((widget) => (
|
||||||
<div
|
<div
|
||||||
@@ -3617,6 +3619,8 @@ export function HostManagerEditor({
|
|||||||
t("serverStats.systemInfo")}
|
t("serverStats.systemInfo")}
|
||||||
{widget === "login_stats" &&
|
{widget === "login_stats" &&
|
||||||
t("serverStats.loginStats")}
|
t("serverStats.loginStats")}
|
||||||
|
{widget === "firewall" &&
|
||||||
|
t("serverStats.firewall.title")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
ProcessesWidget,
|
ProcessesWidget,
|
||||||
SystemWidget,
|
SystemWidget,
|
||||||
LoginStatsWidget,
|
LoginStatsWidget,
|
||||||
|
FirewallWidget,
|
||||||
} from "./widgets";
|
} from "./widgets";
|
||||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||||
|
|
||||||
@@ -159,6 +160,11 @@ export function ServerStats({
|
|||||||
<LoginStatsWidget metrics={metrics} metricsHistory={metricsHistory} />
|
<LoginStatsWidget metrics={metrics} metricsHistory={metricsHistory} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "firewall":
|
||||||
|
return (
|
||||||
|
<FirewallWidget metrics={metrics} metricsHistory={metricsHistory} />
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
213
src/ui/desktop/apps/server-stats/widgets/FirewallWidget.tsx
Normal file
213
src/ui/desktop/apps/server-stats/widgets/FirewallWidget.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Shield, ShieldOff, ShieldCheck, ChevronDown } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
||||||
|
import type {
|
||||||
|
FirewallMetrics,
|
||||||
|
FirewallChain,
|
||||||
|
FirewallRule,
|
||||||
|
} from "@/types/stats-widgets";
|
||||||
|
|
||||||
|
interface FirewallWidgetProps {
|
||||||
|
metrics: ServerMetrics | null;
|
||||||
|
metricsHistory: ServerMetrics[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleRow({ rule }: { rule: FirewallRule }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const getTargetStyle = (target: string) => {
|
||||||
|
switch (target.toUpperCase()) {
|
||||||
|
case "ACCEPT":
|
||||||
|
return "text-green-400";
|
||||||
|
case "DROP":
|
||||||
|
return "text-red-400";
|
||||||
|
case "REJECT":
|
||||||
|
return "text-orange-400";
|
||||||
|
default:
|
||||||
|
return "text-muted-foreground";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTargetLabel = (target: string) => {
|
||||||
|
switch (target.toUpperCase()) {
|
||||||
|
case "ACCEPT":
|
||||||
|
return t("serverStats.firewall.accept");
|
||||||
|
case "DROP":
|
||||||
|
return t("serverStats.firewall.drop");
|
||||||
|
case "REJECT":
|
||||||
|
return t("serverStats.firewall.reject");
|
||||||
|
default:
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSource = () => {
|
||||||
|
if (rule.interface) {
|
||||||
|
return rule.interface;
|
||||||
|
}
|
||||||
|
if (rule.state) {
|
||||||
|
return rule.state;
|
||||||
|
}
|
||||||
|
if (rule.source === "0.0.0.0/0") {
|
||||||
|
return t("serverStats.firewall.anywhere");
|
||||||
|
}
|
||||||
|
return rule.source;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-4 gap-2 text-xs py-1.5 border-b border-edge/30 last:border-0">
|
||||||
|
<div className={`font-medium ${getTargetStyle(rule.target)}`}>
|
||||||
|
{getTargetLabel(rule.target)}
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground-subtle font-mono">
|
||||||
|
{rule.protocol.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground-subtle font-mono">
|
||||||
|
{rule.dport || "-"}
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground-subtle truncate" title={formatSource()}>
|
||||||
|
{formatSource()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChainSection({ chain }: { chain: FirewallChain }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isOpen, setIsOpen] = React.useState(true);
|
||||||
|
|
||||||
|
const getPolicyStyle = (policy: string) => {
|
||||||
|
switch (policy.toUpperCase()) {
|
||||||
|
case "ACCEPT":
|
||||||
|
return "text-green-400";
|
||||||
|
case "DROP":
|
||||||
|
return "text-red-400";
|
||||||
|
case "REJECT":
|
||||||
|
return "text-orange-400";
|
||||||
|
default:
|
||||||
|
return "text-muted-foreground";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="flex items-center gap-2 w-full py-1.5 hover:bg-canvas/30 rounded px-1 -mx-1 text-left"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-3 w-3 text-muted-foreground transition-transform ${
|
||||||
|
isOpen ? "" : "-rotate-90"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
{chain.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
({t("serverStats.firewall.policy")}:{" "}
|
||||||
|
<span className={getPolicyStyle(chain.policy)}>{chain.policy}</span>)
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
|
{chain.rules.length} {t("serverStats.firewall.rules")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{chain.rules.length > 0 ? (
|
||||||
|
<div className="mt-2 ml-5">
|
||||||
|
<div className="grid grid-cols-4 gap-2 text-xs text-muted-foreground border-b border-edge/50 pb-1 mb-1">
|
||||||
|
<div>{t("serverStats.firewall.action")}</div>
|
||||||
|
<div>{t("serverStats.firewall.protocol")}</div>
|
||||||
|
<div>{t("serverStats.firewall.port")}</div>
|
||||||
|
<div>{t("serverStats.firewall.source")}</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-32 overflow-y-auto thin-scrollbar">
|
||||||
|
{chain.rules.map((rule, idx) => (
|
||||||
|
<RuleRow key={idx} rule={rule} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-muted-foreground ml-5 mt-1">
|
||||||
|
{t("serverStats.firewall.noRules")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FirewallWidget({ metrics }: FirewallWidgetProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const firewall = (
|
||||||
|
metrics as ServerMetrics & { firewall?: FirewallMetrics }
|
||||||
|
)?.firewall;
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
if (!firewall || firewall.type === "none") {
|
||||||
|
return <ShieldOff className="h-5 w-5 text-muted-foreground" />;
|
||||||
|
}
|
||||||
|
if (firewall.status === "active") {
|
||||||
|
return <ShieldCheck className="h-5 w-5 text-green-400" />;
|
||||||
|
}
|
||||||
|
return <Shield className="h-5 w-5 text-orange-400" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (!firewall || firewall.type === "none") {
|
||||||
|
return t("serverStats.firewall.notDetected");
|
||||||
|
}
|
||||||
|
if (firewall.status === "active") {
|
||||||
|
return t("serverStats.firewall.active");
|
||||||
|
}
|
||||||
|
return t("serverStats.firewall.inactive");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full p-4 rounded-lg bg-canvas/50 border border-edge/50 hover:bg-canvas/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
|
{getStatusIcon()}
|
||||||
|
<h3 className="font-semibold text-lg text-foreground">
|
||||||
|
{t("serverStats.firewall.title")}
|
||||||
|
</h3>
|
||||||
|
{firewall && firewall.type !== "none" && (
|
||||||
|
<span className="text-xs text-muted-foreground ml-auto bg-canvas/50 px-2 py-0.5 rounded">
|
||||||
|
{firewall.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-3 flex-shrink-0">
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
firewall?.status === "active"
|
||||||
|
? "text-green-400"
|
||||||
|
: firewall?.status === "inactive"
|
||||||
|
? "text-orange-400"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getStatusText()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{firewall && firewall.chains.length > 0 ? (
|
||||||
|
<div className="flex-1 overflow-y-auto thin-scrollbar space-y-2">
|
||||||
|
{firewall.chains.map((chain) => (
|
||||||
|
<ChainSection key={chain.name} chain={chain} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("serverStats.firewall.noData")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ export { UptimeWidget } from "./UptimeWidget";
|
|||||||
export { ProcessesWidget } from "./ProcessesWidget";
|
export { ProcessesWidget } from "./ProcessesWidget";
|
||||||
export { SystemWidget } from "./SystemWidget";
|
export { SystemWidget } from "./SystemWidget";
|
||||||
export { LoginStatsWidget } from "./LoginStatsWidget";
|
export { LoginStatsWidget } from "./LoginStatsWidget";
|
||||||
|
export { FirewallWidget } from "./FirewallWidget";
|
||||||
|
|||||||
Reference in New Issue
Block a user