feat(api, web): implement port ACL and host firewall snapshot features
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m43s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added support for managing desired L4 port ACL rules for Linux agents, allowing for open/close actions on specified ports.
- Introduced a new endpoint for CRUD operations on port rules, enhancing the API's capabilities for agent management.
- Implemented functionality to collect and report host firewall snapshots, capturing observed rules and listeners for better monitoring.
- Updated the agent detail view to include tabs for managing port ACLs and viewing host firewall data, improving user experience.
- Enhanced documentation to reflect the new features and API changes, ensuring clarity for users and developers.

These changes significantly improve the management and visibility of firewall rules and port access control for agents.
This commit is contained in:
Denozordec
2026-08-11 15:08:00 +07:00
parent c5069fbdaf
commit 43f5ac2525
22 changed files with 2853 additions and 17 deletions
+30
View File
@@ -77,6 +77,18 @@ export type {
PortBlockPerIpRow,
} from './stats.js'
export {
listAgentPortRules,
listEnabledAgentPortRules,
getAgentPortRule,
insertAgentPortRule,
updateAgentPortRule,
deleteAgentPortRule,
upsertHostFirewallSnapshot,
getHostFirewallSnapshot,
} from './port-acl.js'
export type { AgentPortRuleRow, AgentPortRuleInsert } from './port-acl.js'
export {
getSetting,
setSetting,
@@ -161,6 +173,16 @@ import {
deletePortBlockStatsForAgent,
resetPortBlockStatsBaselines,
} from './stats.js'
import {
listAgentPortRules,
listEnabledAgentPortRules,
getAgentPortRule,
insertAgentPortRule,
updateAgentPortRule,
deleteAgentPortRule,
upsertHostFirewallSnapshot,
getHostFirewallSnapshot,
} from './port-acl.js'
import {
getSetting,
setSetting,
@@ -236,6 +258,14 @@ export const repos = {
mapTopPortsByIp,
deletePortBlockStatsForAgent,
resetPortBlockStatsBaselines,
listAgentPortRules,
listEnabledAgentPortRules,
getAgentPortRule,
insertAgentPortRule,
updateAgentPortRule,
deleteAgentPortRule,
upsertHostFirewallSnapshot,
getHostFirewallSnapshot,
getSetting,
setSetting,
listSettings,
+93
View File
@@ -0,0 +1,93 @@
import { and, asc, eq } from 'drizzle-orm'
import type { Db } from '../client.js'
import { agentHostFirewallSnapshots, agentPortRules } from '../schema.js'
export type AgentPortRuleRow = typeof agentPortRules.$inferSelect
export type AgentPortRuleInsert = typeof agentPortRules.$inferInsert
export function listAgentPortRules(db: Db, agentId: string) {
return db
.select()
.from(agentPortRules)
.where(eq(agentPortRules.agentId, agentId))
.orderBy(asc(agentPortRules.priority), asc(agentPortRules.createdAt))
.all()
}
export function listEnabledAgentPortRules(db: Db, agentId: string) {
return db
.select()
.from(agentPortRules)
.where(
and(eq(agentPortRules.agentId, agentId), eq(agentPortRules.enabled, 1)),
)
.orderBy(asc(agentPortRules.priority), asc(agentPortRules.createdAt))
.all()
}
export function getAgentPortRule(db: Db, id: string) {
return db.select().from(agentPortRules).where(eq(agentPortRules.id, id)).get()
}
export function insertAgentPortRule(db: Db, row: AgentPortRuleInsert) {
db.insert(agentPortRules).values(row).run()
return getAgentPortRule(db, row.id)
}
export function updateAgentPortRule(
db: Db,
id: string,
patch: Partial<
Omit<AgentPortRuleInsert, 'id' | 'agentId' | 'createdAt'>
>,
) {
db.update(agentPortRules)
.set({
...patch,
updatedAt: new Date().toISOString(),
})
.where(eq(agentPortRules.id, id))
.run()
return getAgentPortRule(db, id)
}
export function deleteAgentPortRule(db: Db, id: string) {
db.delete(agentPortRules).where(eq(agentPortRules.id, id)).run()
}
export function upsertHostFirewallSnapshot(
db: Db,
agentId: string,
payloadJson: string,
collectedAt = new Date().toISOString(),
rawDigest: string | null = null,
) {
const existing = db
.select()
.from(agentHostFirewallSnapshots)
.where(eq(agentHostFirewallSnapshots.agentId, agentId))
.get()
if (existing) {
db.update(agentHostFirewallSnapshots)
.set({ payloadJson, collectedAt, rawDigest })
.where(eq(agentHostFirewallSnapshots.agentId, agentId))
.run()
} else {
db.insert(agentHostFirewallSnapshots)
.values({ agentId, payloadJson, collectedAt, rawDigest })
.run()
}
return db
.select()
.from(agentHostFirewallSnapshots)
.where(eq(agentHostFirewallSnapshots.agentId, agentId))
.get()
}
export function getHostFirewallSnapshot(db: Db, agentId: string) {
return db
.select()
.from(agentHostFirewallSnapshots)
.where(eq(agentHostFirewallSnapshots.agentId, agentId))
.get()
}
+56
View File
@@ -264,6 +264,60 @@ export const agentPortBlockStats = sqliteTable(
}),
)
/** Desired L4 port ACL per Linux agent (open/close). */
export const agentPortRules = sqliteTable(
'agent_port_rules',
{
id: text('id').primaryKey(),
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
action: text('action').notNull(), // open | close
protocol: text('protocol').notNull(), // tcp | udp | both
portStart: integer('port_start').notNull(),
portEnd: integer('port_end').notNull(),
srcKind: text('src_kind').notNull(), // all | cidr | list
srcCidr: text('src_cidr'),
listId: text('list_id').references(() => ipLists.id, {
onDelete: 'set null',
}),
enabled: integer('enabled').notNull().default(1),
comment: text('comment'),
priority: integer('priority').notNull().default(100),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
agentPriority: index('idx_agent_port_rules_agent_priority').on(
t.agentId,
t.priority,
),
agentEnabled: index('idx_agent_port_rules_agent_enabled').on(
t.agentId,
t.enabled,
),
}),
)
/** Latest observed host firewall + listeners snapshot from Linux agent. */
export const agentHostFirewallSnapshots = sqliteTable(
'agent_host_firewall_snapshots',
{
agentId: text('agent_id')
.primaryKey()
.references(() => agents.id, { onDelete: 'cascade' }),
collectedAt: text('collected_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
payloadJson: text('payload_json').notNull(),
rawDigest: text('raw_digest'),
},
)
/** Short install invite links (`/agent-install/:id` and `/:slug`). */
export const agentInstallLinks = sqliteTable(
'agent_install_links',
@@ -328,6 +382,8 @@ export const schema = {
agentStatsSamples,
agentIpBlockStats,
agentPortBlockStats,
agentPortRules,
agentHostFirewallSnapshots,
agentInstallLinks,
auditLog,
}