Add a Network > Ports page: agent-reported ports plus manual openings
Summarizes every server's agent-reported listening ports in one cross-server table (grouped by protocol+port, addresses merged, loopback-only flagged) - previously this only existed per-server on each server's own detail page. Adds a second table for ports this app has no way to see on its own: manually-recorded openings on a router, edge firewall, or cloud security group, each with a label, external port/protocol, an optional link to a tracked server (with its own internal port when NAT changes it) or a freeform destination, a free-text source, and a comment. Viewer-readable; adding/editing/deleting needs operator or admin. The agent-port grouping logic (dedupe by protocol+port, detect loopback-only sockets) was shared with the existing per-server Ports card via a new agentPorts.ts service instead of duplicating it. Verified with a real HTTP-level test: a genuine Express app with the actual routers, a scratch SQLite DB, and forged admin/viewer sessions, covering grouping correctness, the server-name join, input validation, and role enforcement - the real dev DB was confirmed untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e48377348
commit
236b1da0dc
14 files changed
+2486
-39
No files matched your search
@@ -0,0 +1,53 @@
|
||||
// Shared between the per-server Ports card (routes/serverPorts.ts) and the cross-server
|
||||
// Network > Ports page (routes/ports.ts) — both read the same agent-reported "listeningPorts"
|
||||
// JSON column and need the same one-row-per-socket -> one-row-per-protocol+port grouping.
|
||||
|
||||
export interface AgentPort {
|
||||
protocol: "tcp" | "udp";
|
||||
port: number;
|
||||
address: string;
|
||||
process?: string;
|
||||
}
|
||||
|
||||
export interface GroupedAgentPort {
|
||||
addresses: string[];
|
||||
process: string | null;
|
||||
localOnly: boolean;
|
||||
}
|
||||
|
||||
export function parseJson<T>(text: string | null | undefined, fallback: T): T {
|
||||
if (!text) return fallback;
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoopback(address: string): boolean {
|
||||
const bare = address.replace(/%.*$/, "").replace(/^\[|\]$/g, "");
|
||||
return bare.startsWith("127.") || bare === "::1";
|
||||
}
|
||||
|
||||
/** Groups the agent's raw one-row-per-socket report into one entry per protocol+port, keyed "tcp:443". */
|
||||
export function groupAgentPorts(raw: AgentPort[]): Map<string, GroupedAgentPort> {
|
||||
const grouped = new Map<string, { addresses: Set<string>; process: string | null }>();
|
||||
for (const p of raw) {
|
||||
const key = `${p.protocol}:${p.port}`;
|
||||
const entry = grouped.get(key) ?? { addresses: new Set<string>(), process: null };
|
||||
entry.addresses.add(p.address);
|
||||
if (!entry.process && p.process) entry.process = p.process;
|
||||
grouped.set(key, entry);
|
||||
}
|
||||
const out = new Map<string, GroupedAgentPort>();
|
||||
for (const [key, entry] of grouped) {
|
||||
const addresses = [...entry.addresses];
|
||||
out.set(key, { addresses, process: entry.process, localOnly: addresses.every(isLoopback) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function splitPortKey(key: string): { protocol: "tcp" | "udp"; port: number } {
|
||||
const [protocol, port] = key.split(":");
|
||||
return { protocol: protocol as "tcp" | "udp", port: Number(port) };
|
||||
}
|
||||
Reference in new issue
Block a user